Skip to content

Commit 77946e7

Browse files
committed
improve dependency version constraint and errors
Overhauls the parsing of the Dependency structs and subfields to actually use semver constraint checks and improve the error messages returned when an invalid dependency struct is specified or a dependency version doesn't meet the specified constraint, as shown in this example: ``` $ gdt run scenario/testdata/depends-not-satisfied-version-constraint.yaml Error: runtime error: dependency not satisfied: "ls" failed version constraint ">99.9.9" ``` Issue gdt-dev/gdt#60 Signed-off-by: Jay Pipes <jaypipes@gmail.com>
1 parent 4d4d7da commit 77946e7

15 files changed

Lines changed: 542 additions & 32 deletions

README.md

Lines changed: 61 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,17 +26,26 @@ All `gdt` scenarios have the following fields:
2626
* `fixtures`: (optional) list of strings indicating named fixtures that will be
2727
started before any of the tests in the file are run
2828
* `depends`: (optional) list of [`Dependency`][dependency] objects that
29-
describe a program or package that should be available in the host's `PATH`
29+
describe a program binary that should be available in the host's `PATH`
3030
that the test scenario depends on.
31-
* `depends.name`: string name of the program or package the test scenario
32-
depends on.
31+
* `depends.name`: string name of the program the test scenario depends on.
3332
* `depends.when`: (optional) object describing any constraints/conditions that
3433
should apply to the evaluation of the dependency.
3534
* `depends.when.os`: (optional) string operating system. if set, the dependency
3635
is only checked for that OS.
37-
* `depends.when.version`: (optional) string version constraint. if set, the
36+
* `depends.version`: (optional) struct containing version constraint and
37+
selector instructions.
38+
* `depends.version.constraint`: optional string version constraint. if set, the
3839
program or package must be available on the host `PATH` and must satisfy the
3940
version constraint.
41+
* `depends.version.selector`: (optional) struct containing selector
42+
instructions for gdt to determine the version of a dependent binary.
43+
* `depends.version.selector.args`: (optional) set of string arguments to call
44+
the dependency binary with in order to get the program's version information.
45+
If empty, we default to []string{`-v`}.
46+
* `depends.version.selector.filter`: (optional) regular expression to run
47+
against the returned output from executing the dependency binary with the
48+
`selector.args` arguments. If empty, we use a loose semver matching regex.
4049
* `skip-if`: (optional) list of [`Spec`][basespec] specializations that will be
4150
evaluated *before* running any test in the scenario. If any of these
4251
conditions evaluates successfully, the test scenario will be skipped.
@@ -249,6 +258,54 @@ $ gdt run myapp.yaml
249258
Error: runtime error: exec: "myapp": executable file not found in $PATH
250259
```
251260
261+
You may also specify a particular version constraint that must pass for a
262+
dependent binary with the `depends.version.constraint` field. For example,
263+
let's assume I want to declare my test scenario requires that at least version
264+
`1.2.3` of `myapp` must be present on the host machine, I would do this:
265+
266+
```yaml
267+
depends:
268+
- name: myapp
269+
version:
270+
constraint: ">=1.2.3"
271+
```
272+
273+
The `depends.version.constraint` field should be a valid Semantic Versioning
274+
constraint. Read more about [Semantic Version constraints][semver-constraints].
275+
276+
By default to determine a binary's version, we pass a `-v` flag to the binary
277+
itself. If you know that a binary uses a different way of returning its version
278+
information, you can use the `depends.version.selector.args` field. As an
279+
example, the `ls` command line utility on Linux returns its version information
280+
when you pass the `--version` CLI flag, as shown here:
281+
282+
```
283+
> ls --version
284+
ls (GNU coreutils) 9.4
285+
Copyright (C) 2023 Free Software Foundation, Inc.
286+
License GPLv3+: GNU GPL version 3 or later <https://gnu.org/licenses/gpl.html>.
287+
This is free software: you are free to change and redistribute it.
288+
There is NO WARRANTY, to the extent permitted by law.
289+
290+
Written by Richard M. Stallman and David MacKenzie.
291+
```
292+
293+
If you wanted to require that, say, version 9.1 and later of the `ls`
294+
command-line utility was present on the host machine, you would do the
295+
following:
296+
297+
```yaml
298+
depends:
299+
- name: ls
300+
version:
301+
constraint: ">=9.1"
302+
selector:
303+
args:
304+
- "--version"
305+
```
306+
307+
[semver-constraints]: https://github.com/Masterminds/semver/blob/master/README.md#checking-version-constraints
308+
252309
### Passing variables to subsequent test specs
253310

254311
A `gdt` test scenario is comprised of a list of test specs. These test specs

api/dependency.go

Lines changed: 207 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,220 @@
11
package api
22

3-
// Dependency describes a prerequisite binary or package that must be present.
3+
import (
4+
"regexp"
5+
6+
"github.com/Masterminds/semver/v3"
7+
"github.com/samber/lo"
8+
"gopkg.in/yaml.v3"
9+
10+
"github.com/gdt-dev/core/parse"
11+
)
12+
13+
var (
14+
ValidOSs = []string{
15+
"linux",
16+
"darwin",
17+
"windows",
18+
}
19+
)
20+
21+
// Dependency describes a prerequisite binary that must be present.
422
type Dependency struct {
5-
// Name is the name of the binary or package
23+
// Name is the name of the binary that must be present.
624
Name string `yaml:"name"`
725
// When describes any constraining conditions that apply to this
826
// Dependency.
9-
When *DependencyConstraints `yaml:"when,omitempty"`
27+
When *DependencyConditions `yaml:"when,omitempty"`
28+
// Version contains instructions for constraining and selecting the
29+
// dependency's version.
30+
Version *DependencyVersion `yaml:"version,omitempty"`
1031
}
1132

12-
// DependencyConstraints describes constraining conditions that apply to a
33+
func (d *Dependency) UnmarshalYAML(node *yaml.Node) error {
34+
if node.Kind != yaml.MappingNode {
35+
return parse.ExpectedMapAt(node)
36+
}
37+
for i := 0; i < len(node.Content); i += 2 {
38+
keyNode := node.Content[i]
39+
if keyNode.Kind != yaml.ScalarNode {
40+
return parse.ExpectedScalarAt(keyNode)
41+
}
42+
key := keyNode.Value
43+
valNode := node.Content[i+1]
44+
switch key {
45+
case "name":
46+
if valNode.Kind != yaml.ScalarNode {
47+
return parse.ExpectedScalarAt(valNode)
48+
}
49+
d.Name = valNode.Value
50+
case "when":
51+
if valNode.Kind != yaml.MappingNode {
52+
return parse.ExpectedMapAt(valNode)
53+
}
54+
var when DependencyConditions
55+
if err := valNode.Decode(&when); err != nil {
56+
return err
57+
}
58+
d.When = &when
59+
case "version":
60+
if valNode.Kind != yaml.MappingNode {
61+
return parse.ExpectedMapAt(valNode)
62+
}
63+
var dv DependencyVersion
64+
if err := valNode.Decode(&dv); err != nil {
65+
return err
66+
}
67+
d.Version = &dv
68+
default:
69+
return parse.UnknownFieldAt(key, keyNode)
70+
}
71+
}
72+
return nil
73+
}
74+
75+
// DependencyConditions describes constraining conditions that apply to a
1376
// Dependency, for instance whether the dependency is only required on a
14-
// particular OS or whether a particular version constraint applies to the
15-
// dependency.
16-
type DependencyConstraints struct {
77+
// particular OS.
78+
type DependencyConditions struct {
1779
// OS indicates that the dependency only applies when the tests are run on
1880
// a particular operating system.
1981
OS string `yaml:"os,omitempty"`
20-
// Version indicates a version constraint to apply to the dependency, e.g.
21-
// >= 1.2.3
22-
Version string `yaml:"version,omitempty"`
82+
}
83+
84+
func (c *DependencyConditions) UnmarshalYAML(node *yaml.Node) error {
85+
if node.Kind != yaml.MappingNode {
86+
return parse.ExpectedMapAt(node)
87+
}
88+
for i := 0; i < len(node.Content); i += 2 {
89+
keyNode := node.Content[i]
90+
if keyNode.Kind != yaml.ScalarNode {
91+
return parse.ExpectedScalarAt(keyNode)
92+
}
93+
key := keyNode.Value
94+
valNode := node.Content[i+1]
95+
switch key {
96+
case "os":
97+
if valNode.Kind != yaml.ScalarNode {
98+
return parse.ExpectedScalarAt(valNode)
99+
}
100+
os := valNode.Value
101+
if os != "" {
102+
if !lo.Contains(ValidOSs, os) {
103+
return parse.InvalidOSAt(valNode, os, ValidOSs)
104+
}
105+
c.OS = os
106+
}
107+
default:
108+
return parse.UnknownFieldAt(key, keyNode)
109+
}
110+
}
111+
return nil
112+
}
113+
114+
// DependencyVersion expresses a version constraint that must be met for a
115+
// particular dependency and instructs gdt how to get the version for a
116+
// dependency from a binary or package manager.
117+
type DependencyVersion struct {
118+
// Constraint indicates a version constraint to apply to the dependency,
119+
// e.g. '>= 1.2.3' would indicate that a version of the dependency binary
120+
// after and including 1.2.3 must be present on the host.
121+
Constraint string `yaml:"constraint"`
122+
SemVerConstraints *semver.Constraints `yaml:"-"`
123+
// Selector provides instructions to select the version from the binary.
124+
Selector *DependencyVersionSelector `yaml:"selector,omitempty"`
125+
}
126+
127+
func (v *DependencyVersion) UnmarshalYAML(node *yaml.Node) error {
128+
if node.Kind != yaml.MappingNode {
129+
return parse.ExpectedMapAt(node)
130+
}
131+
for i := 0; i < len(node.Content); i += 2 {
132+
keyNode := node.Content[i]
133+
if keyNode.Kind != yaml.ScalarNode {
134+
return parse.ExpectedScalarAt(keyNode)
135+
}
136+
key := keyNode.Value
137+
valNode := node.Content[i+1]
138+
switch key {
139+
case "constraint":
140+
if valNode.Kind != yaml.ScalarNode {
141+
return parse.ExpectedScalarAt(valNode)
142+
}
143+
conStr := valNode.Value
144+
if conStr != "" {
145+
con, err := semver.NewConstraint(conStr)
146+
if err != nil {
147+
return parse.InvalidVersionConstraintAt(
148+
valNode, conStr, err,
149+
)
150+
}
151+
v.Constraint = conStr
152+
v.SemVerConstraints = con
153+
}
154+
case "selector":
155+
if valNode.Kind != yaml.MappingNode {
156+
return parse.ExpectedMapAt(valNode)
157+
}
158+
var selector DependencyVersionSelector
159+
if err := valNode.Decode(&selector); err != nil {
160+
return err
161+
}
162+
v.Selector = &selector
163+
default:
164+
return parse.UnknownFieldAt(key, keyNode)
165+
}
166+
}
167+
return nil
168+
}
169+
170+
// DependencyVersionSelector instructs gdt how to get the version of a binary.
171+
type DependencyVersionSelector struct {
172+
// Args is the command-line to execute the dependency binary to output
173+
// version information, e.g. '-v' or '--version-json'.
174+
Args []string `yaml:"args,omitempty"`
175+
// Filter is an optional regex to run against the output returned by
176+
// Command, e.g. 'v?(\d)+\.(\d+)(\.(\d)+)?'.
177+
Filter string `yaml:"filter,omitempty"`
178+
FilterRegex *regexp.Regexp `yaml:"-"`
179+
}
180+
181+
func (s *DependencyVersionSelector) UnmarshalYAML(node *yaml.Node) error {
182+
if node.Kind != yaml.MappingNode {
183+
return parse.ExpectedMapAt(node)
184+
}
185+
for i := 0; i < len(node.Content); i += 2 {
186+
keyNode := node.Content[i]
187+
if keyNode.Kind != yaml.ScalarNode {
188+
return parse.ExpectedScalarAt(keyNode)
189+
}
190+
key := keyNode.Value
191+
valNode := node.Content[i+1]
192+
switch key {
193+
case "args":
194+
if valNode.Kind != yaml.SequenceNode {
195+
return parse.ExpectedSequenceAt(valNode)
196+
}
197+
var args []string
198+
if err := valNode.Decode(&args); err != nil {
199+
return err
200+
}
201+
s.Args = args
202+
case "filter":
203+
if valNode.Kind != yaml.ScalarNode {
204+
return parse.ExpectedMapAt(valNode)
205+
}
206+
filter := valNode.Value
207+
if filter != "" {
208+
re, err := regexp.Compile(filter)
209+
if err != nil {
210+
return parse.InvalidRegexAt(valNode, filter, err)
211+
}
212+
s.Filter = filter
213+
s.FilterRegex = re
214+
}
215+
default:
216+
return parse.UnknownFieldAt(key, keyNode)
217+
}
218+
}
219+
return nil
23220
}

api/error.go

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -147,19 +147,29 @@ var (
147147
// DependencyNotSatified returns an ErrDependencyNotSatisfied with the supplied
148148
// dependency name and optional constraints.
149149
func DependencyNotSatisfied(dep *Dependency) error {
150-
constraintsStr := ""
151-
constraints := []string{}
150+
conditionsStr := ""
151+
conditions := []string{}
152152
progName := dep.Name
153153
if dep.When != nil {
154154
if dep.When.OS != "" {
155-
constraints = append(constraints, "OS:"+dep.When.OS)
155+
conditions = append(conditions, "OS:"+dep.When.OS)
156156
}
157-
if dep.When.Version != "" {
158-
constraints = append(constraints, "VERSION:"+dep.When.Version)
159-
}
160-
constraintsStr = fmt.Sprintf(" (%s)", strings.Join(constraints, ","))
161157
}
162-
return fmt.Errorf("%w: %s%s", ErrDependencyNotSatisfied, progName, constraintsStr)
158+
conditionsStr = fmt.Sprintf(" (%s)", strings.Join(conditions, ","))
159+
return fmt.Errorf("%w: %s%s", ErrDependencyNotSatisfied, progName, conditionsStr)
160+
}
161+
162+
// DependencyNotSatifiedVersionConstraint returns an ErrDependencyNotSatisfied with the supplied
163+
// dependency name and version constraint failure.
164+
func DependencyNotSatisfiedVersionConstraint(
165+
dep *Dependency,
166+
constraintStr string,
167+
) error {
168+
progName := dep.Name
169+
return fmt.Errorf(
170+
"%w: %q failed version constraint %q",
171+
ErrDependencyNotSatisfied, progName, constraintStr,
172+
)
163173
}
164174

165175
// RequiredFixtureMissing returns an ErrRequiredFixture with the supplied

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ module github.com/gdt-dev/core
33
go 1.24.3
44

55
require (
6+
github.com/Masterminds/semver/v3 v3.4.0
67
github.com/cenkalti/backoff v2.2.1+incompatible
78
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
89
github.com/google/uuid v1.6.0

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
2+
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
13
github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4=
24
github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
35
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=

0 commit comments

Comments
 (0)