Skip to content

Commit 7c05a0e

Browse files
authored
Merge pull request #17 from jaypipes/jaypipes/depends
improve dependency version constraint and errors
2 parents c6dd6b5 + 77946e7 commit 7c05a0e

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)