diff --git a/go.mod b/go.mod index 566e99fb4100d8110b596b47618e99840b61bd06..ed9d811c4a262cf6f54b95ab45eb7ed7819a7f79 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module gitlab.hedenstroem.com/go/snok go 1.22.3 require ( + github.com/docker/docker v25.0.5+incompatible github.com/spf13/cobra v1.8.0 github.com/spf13/pflag v1.0.5 github.com/stretchr/testify v1.9.0 @@ -22,7 +23,6 @@ require ( github.com/cpuguy83/dockercfg v0.3.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/distribution/reference v0.5.0 // indirect - github.com/docker/docker v25.0.5+incompatible // indirect github.com/docker/go-connections v0.5.0 // indirect github.com/docker/go-units v0.5.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect diff --git a/helpers.go b/helpers.go index 93f26623bff2ca5fb6b7ba4e3bdfb34cf4a8a01f..0458e34a96831939b3af07c9555087196cdecda0 100644 --- a/helpers.go +++ b/helpers.go @@ -8,8 +8,8 @@ import ( "github.com/stretchr/testify/require" ) -func PrintEnv(tests *Tests, t *testing.T, args []string) { - if tests.Debug { +func PrintEnv(s *TestSuite, t *testing.T, args []string) { + if s.GetLogLevel() >= DebugLevel { t.Helper() envs := os.Environ() sort.Strings(envs) @@ -19,7 +19,7 @@ func PrintEnv(tests *Tests, t *testing.T, args []string) { } } -func TempFile(tests *Tests, t *testing.T, args []string) { +func TempFile(s *TestSuite, t *testing.T, args []string) { t.Helper() file, err := os.CreateTemp("", "test_") require.NoError(t, err) diff --git a/logging.go b/logging.go new file mode 100644 index 0000000000000000000000000000000000000000..c61cfe185091ec111d9119c95c81b6e2c3e9469c --- /dev/null +++ b/logging.go @@ -0,0 +1,50 @@ +package snok + +import ( + "fmt" + "strings" + "testing" + + "github.com/testcontainers/testcontainers-go" +) + +type LogLevel uint32 + +const ( + ErrorLevel LogLevel = iota + WarnLevel + InfoLevel + DebugLevel + TraceLevel +) + +func ParseLogLevel(lvl string) (LogLevel, error) { + switch strings.ToLower(lvl) { + case "error": + return ErrorLevel, nil + case "warn", "warning": + return WarnLevel, nil + case "info": + return InfoLevel, nil + case "debug": + return DebugLevel, nil + case "trace": + return TraceLevel, nil + default: + return InfoLevel, fmt.Errorf("uknown log level %q, using info.", lvl) + } +} + +type TestLogConsumer struct { + t *testing.T + testcontainers.LogConsumer +} + +func NewTestLogConsumer(t *testing.T) *TestLogConsumer { + return &TestLogConsumer{t: t} +} + +func (l *TestLogConsumer) Accept(log testcontainers.Log) { + l.t.Helper() + l.t.Logf("%s", log.Content) +} diff --git a/snok.go b/snok.go deleted file mode 100644 index a94367276aae4a0e5c14899de512652c8cdb2144..0000000000000000000000000000000000000000 --- a/snok.go +++ /dev/null @@ -1,201 +0,0 @@ -package snok - -import ( - "bytes" - "encoding/json" - "flag" - "fmt" - "io" - "os" - "regexp" - "strings" - "sync" - "testing" - "unicode" - - "github.com/spf13/cobra" - "github.com/spf13/pflag" - "github.com/stretchr/testify/require" - "gonum.org/v1/gonum/graph" - "gonum.org/v1/gonum/graph/simple" - "gonum.org/v1/gonum/graph/topo" -) - -type Tests struct { - t *testing.T - RootCmd *cobra.Command - Debug bool - Trace bool - graph *simple.DirectedGraph - unexpectedErr func(require.TestingT, error, ...interface{}) -} - -func NewTestSuite(cmd *cobra.Command) *Tests { - ct := &Tests{ - RootCmd: cmd, - Debug: false, - Trace: false, - graph: simple.NewDirectedGraph(), - unexpectedErr: require.NoError, - } - flag.BoolVar(&ct.Debug, "debug", false, "debug") - flag.BoolVar(&ct.Trace, "trace", false, "trace") - flag.Parse() - if os.Getenv("SNOK_DEBUG") == "true" { - ct.Debug = true - } - if os.Getenv("SNOK_TRACE") == "true" { - ct.Trace = true - } - return ct -} - -func (ct *Tests) GetTest(name string) (*test, error) { - node, new := ct.graph.NodeWithID(hash(name)) - if new { - return nil, fmt.Errorf("undefined test: %s", name) - } - if test, ok := node.(*test); ok { - return test, nil - } - return nil, fmt.Errorf("node type is not test: %s", name) -} - -func (tests *Tests) AddHelper(name string, fn func(*Tests, *testing.T, []string), needs ...string) *helper { - helper := &helper{ - node: node{ - Name: name, - Needs: needs, - }, - fn: fn, - } - tests.graph.AddNode(helper) - return helper -} - -func (tests *Tests) AddHelperCleanup(f func()) { - tests.t.Cleanup(f) -} - -func (tests *Tests) addTests(t *testing.T, cmd *cobra.Command, cmds []string) { - cmds = append(cmds, cmd.Name()) - annotation, exists := cmd.Annotations["tests"] - if exists { - nodes := []test{} - err := json.Unmarshal([]byte(annotation), &nodes) - require.NoError(t, err, "Malformed annotation: %s", annotation) - for _, test := range nodes { - test.cmd = cmd - test.cmds = cmds - tests.graph.AddNode(&test) - } - } - if cmd.HasSubCommands() { - for _, cmd := range cmd.Commands() { - tests.addTests(t, cmd, cmds) - } - } -} - -func (tests *Tests) calculateExecutionOrder(t *testing.T) []graph.Node { - for _, n := range graph.NodesOf(tests.graph.Nodes()) { - if node, ok := n.(Node); ok { - for _, id := range node.DependencyIDs() { - dependency, new := tests.graph.NodeWithID(id) - require.False(t, new, "'%s' has invalid needs", node) - dependency.(Node).SetNeeded(true) - tests.graph.SetEdge(simple.Edge{F: dependency, T: node}) - } - } - } - cycles := topo.DirectedCyclesIn(tests.graph) - require.Empty(t, cycles, "Detected %d cycles", len(cycles)) - sorted, err := topo.Sort(tests.graph) - require.NoError(t, err) - return sorted -} - -func (tests *Tests) Run(t *testing.T) { - tests.t = t - tests.addTests(t, tests.RootCmd, []string{}) - nodes := tests.calculateExecutionOrder(t) - for _, node := range nodes { - if helper, ok := node.(*helper); ok && helper.needed { - t.Run(fmt.Sprintf("[%s]", helper.Name), func(tt *testing.T) { - helper.fn(tests, t, helper.args) - }) - } - if test, ok := node.(*test); ok { - tests.executeTest(t, test) - } - } -} - -func resetFlags(t *testing.T, cmd *cobra.Command) { - cmd.Flags().VisitAll(func(pf *pflag.Flag) { - if pf.Changed { - err := pf.Value.Set(pf.DefValue) - require.NoError(t, err, "Error setting %d to %d", pf.Name, pf.DefValue) - pf.Changed = false - } - }) -} - -func (tests *Tests) executeTest(t *testing.T, test *test) { - t.Run(test.Name, func(t *testing.T) { - var input io.Reader - if test.Input != nil { - input = openURI(t, *test.Input) - defer func() { - if r, ok := input.(io.Closer); ok { - r.Close() - } - }() - } - output, err := tests.executeCmd(append(test.cmds[1:], test.Args...), input) - resetFlags(t, test.cmd) - if test.ExpectError { - require.Error(t, err, "Expected error") - } else if err != nil { - tests.unexpectedErr(t, err, "Unexpected error") - } else if test.Output != nil { - output = strings.TrimRightFunc(output, unicode.IsSpace) - if strings.HasPrefix(*test.Output, "/") && strings.HasSuffix(*test.Output, "/") { - expr := (*test.Output)[1 : len(*test.Output)-1] - re := regexp.MustCompile(expr) - require.True(t, re.MatchString(output), "Expected output to match /%s/, got '%s'", expr, output) - } else { - require.Equal(t, *test.Output, output, "Expected output '%s', got '%s'", *test.Output, output) - } - } - }) -} - -func (tests *Tests) executeCmd(args []string, input io.Reader) (string, error) { - - osStdout := os.Stdout // keep backup of the real stdout - defer func() { os.Stdout = osStdout }() // restore the real stdout - r, w, _ := os.Pipe() - os.Stdout = w - - output := new(bytes.Buffer) - - var wg sync.WaitGroup - wg.Add(1) - go func() { - _, err := io.Copy(output, r) - require.NoError(tests.t, err, "IO Error") - wg.Done() - }() - - tests.RootCmd.SetIn(input) - tests.RootCmd.SetOutput(w) - tests.RootCmd.SetArgs(args) - err := tests.RootCmd.Execute() - - w.Close() - wg.Wait() - - return output.String(), err - -} diff --git a/snok_test.go b/snok_test.go index e61f32f63f08ca05fc768e3a4291837f03c00517..ae32f22efc18f89ad7ba6405f43d8ab6fd47bd8d 100644 --- a/snok_test.go +++ b/snok_test.go @@ -18,7 +18,7 @@ import ( "github.com/testcontainers/testcontainers-go/modules/mockserver" ) -var tests *Tests +var suite *TestSuite func mockUnexpectedError(t require.TestingT, err error, msgAndArgs ...interface{}) { if err.Error() != "expect the unexpected" { @@ -30,30 +30,30 @@ func TestMain(m *testing.M) { rootCmd.AddCommand(envCmd, echoCmd, errorCmd) rootCmd.PersistentFlags().BoolP("debug", "d", false, "debug flag") echoCmd.Flags().BoolP("reverse", "r", false, "reverse the output") - tests = NewTestSuite(rootCmd) - tests.unexpectedErr = mockUnexpectedError - tests.AddHelper("PrintEnv", PrintEnv) - tests.AddHelper("TempFile", TempFile).SetArgs("TEMP_FILE") - tests.AddHelper("TestContainer", mockContainer) - tests.AddHelper("PatchURL", patchTestInput, "TestContainer") + suite = NewTestSuite(rootCmd) + suite.unexpectedErr = mockUnexpectedError + suite.AddHelper("PrintEnv", PrintEnv) + suite.AddHelper("TempFile", TempFile).SetArgs("TEMP_FILE") + suite.AddHelper("TestContainer", mockContainer) + suite.AddHelper("PatchURL", patchTestInput, "TestContainer") os.Exit(m.Run()) } func TestCmds(t *testing.T) { testcontainers.Logger = log.New(&ioutils.NopWriter{}, "", 0) - if tests.Trace { + if suite.GetLogLevel() >= TraceLevel { testcontainers.Logger = log.New(os.Stdout, " ", 0) } - tests.Run(t) + suite.Run(t) } -func mockContainer(tests *Tests, t *testing.T, args []string) { +func mockContainer(s *TestSuite, t *testing.T, args []string) { if os.Getenv("MOCKSERVER_URL") == "" { ctx := context.Background() opts := []testcontainers.ContainerCustomizer{ testcontainers.WithImage("mockserver/mockserver:5.15.0"), } - if tests.Debug || tests.Trace { + if s.GetLogLevel() >= DebugLevel { opts = append(opts, testcontainers.WithLogConsumers(NewTestLogConsumer(t))) } mockserverContainer, err := mockserver.RunContainer(ctx, opts...) @@ -63,19 +63,19 @@ func mockContainer(tests *Tests, t *testing.T, args []string) { require.NoError(t, err) url, err := mockserverContainer.URL(ctx) require.NoError(t, err) - tests.AddHelperCleanup(func() { + s.AddHelperCleanup(func() { require.NoError(t, mockserverContainer.Terminate(ctx)) }) - t.Setenv("MOCKSERVER_URL", url) + s.Setenv("MOCKSERVER_URL", url) } } -func patchTestInput(tests *Tests, t *testing.T, args []string) { - _, err := tests.GetTest("TestContainer") +func patchTestInput(s *TestSuite, t *testing.T, args []string) { + _, err := s.GetTest("TestContainer") require.Error(t, err) - _, err = tests.GetTest("Does not exist") + _, err = s.GetTest("Does not exist") require.Error(t, err) - test, err := tests.GetTest("Echo Input (http)") + test, err := s.GetTest("Echo Input (http)") dashboardUrl := os.Getenv("MOCKSERVER_URL") + "/mockserver/dashboard" t.Logf("Modifying %s", test.String()) test.Input = &dashboardUrl diff --git a/test_command.go b/test_command.go new file mode 100644 index 0000000000000000000000000000000000000000..61b69aeb54ba34e0cd18183308c7e82ab3df69a5 --- /dev/null +++ b/test_command.go @@ -0,0 +1,53 @@ +package snok + +import ( + "encoding/json" + + "github.com/spf13/cobra" +) + +type TestConfiguration struct { + TestName string `json:"name,omitempty"` + Needs []string `json:"needs,omitempty"` + Args []string `json:"args,omitempty"` + Input *string `json:"input,omitempty"` + Output *string `json:"output,omitempty"` + ExpectError bool `json:"expectError,omitempty"` +} + +type TestCommand struct { + TestJob + TestConfiguration + Cmd *cobra.Command + Cmds []string +} + +func NewTestCommand(configuration *TestConfiguration, cmd *cobra.Command) *TestCommand { + job := NewTestJob(configuration.TestName) + return &TestCommand{ + TestJob: *job, + TestConfiguration: *configuration, + Cmd: cmd, + } +} + +func FromAnnotation(annotation string) ([]*TestCommand, error) { + configurations := []*TestConfiguration{} + err := json.Unmarshal([]byte(annotation), &configurations) + if err != nil { + return nil, err + } + tests := make([]*TestCommand, len(configurations)) + for i, configuration := range configurations { + tests[i] = NewTestCommand(configuration, nil) + } + return tests, nil +} + +func (c *TestCommand) SetCmd(cmd *cobra.Command) { + c.Cmd = cmd +} + +func (c *TestCommand) SetCmds(cmds []string) { + c.Cmds = cmds +} diff --git a/test_graph.go b/test_graph.go new file mode 100644 index 0000000000000000000000000000000000000000..68010b5973821e24ceebbaa74adbe322bb24f7e5 --- /dev/null +++ b/test_graph.go @@ -0,0 +1,88 @@ +package snok + +import ( + "fmt" + + "gonum.org/v1/gonum/graph" + "gonum.org/v1/gonum/graph/simple" + "gonum.org/v1/gonum/graph/topo" +) + +type dependency interface { + SetNeeded(bool) +} + +type TestGraph struct { + Graph *simple.DirectedGraph +} + +func NewTestGraph() *TestGraph { + return &TestGraph{simple.NewDirectedGraph()} +} + +func (g *TestGraph) AddJob(job graph.Node) { + g.Graph.AddNode(job) +} + +func (g *TestGraph) GetJob(name string) (any, error) { + job, new := g.Graph.NodeWithID(hash(name)) + if new { + return nil, fmt.Errorf("undefined test: %s", name) + } + switch job := job.(type) { + case *TestCommand: + return job, nil + case *TestHelper: + return job, nil + default: + return nil, fmt.Errorf("%s has unknown job type", name) + } +} + +func (g *TestGraph) DeleteJob(name string) { + g.Graph.RemoveNode(hash(name)) +} + +func (g *TestGraph) ResetDeps() { + for _, edge := range graph.EdgesOf(g.Graph.Edges()) { + g.Graph.RemoveEdge(edge.From().ID(), edge.To().ID()) + } +} + +func (g *TestGraph) ResolveDeps() error { + for _, node := range graph.NodesOf(g.Graph.Nodes()) { + var job *TestJob + var needs []string + switch node := node.(type) { + case *TestCommand: + job = &node.TestJob + needs = node.Needs + case *TestHelper: + job = &node.TestJob + needs = node.Needs + } + if needs == nil { + continue + } + for _, need := range needs { + dep, new := g.Graph.NodeWithID(hash(need)) + if new { + return fmt.Errorf("%s needed by %s is undefined", need, job) + } + if dep.ID() == node.ID() { + return fmt.Errorf("%s cannot need itself", job) + } + dep.(dependency).SetNeeded(true) + g.Graph.SetEdge(simple.Edge{F: dep, T: node}) + } + } + cycles := topo.DirectedCyclesIn(g.Graph) + if len(cycles) > 0 { + return fmt.Errorf("detected %d cycles", len(cycles)) + } + return nil +} + +func (g *TestGraph) SortJobs() ([]graph.Node, error) { + return topo.Sort(g.Graph) +} diff --git a/test_helper.go b/test_helper.go new file mode 100644 index 0000000000000000000000000000000000000000..7efc2964de930750136e954e37fbb405167149ff --- /dev/null +++ b/test_helper.go @@ -0,0 +1,29 @@ +package snok + +import "testing" + +type TestHelperFn func(*TestSuite, *testing.T, []string) + +type TestHelper struct { + TestJob + Fn TestHelperFn + Needs []string + Args []string +} + +func NewTestHelper(name string, fn TestHelperFn, needs ...string) *TestHelper { + job := NewTestJob(name) + return &TestHelper{ + TestJob: *job, + Fn: fn, + Needs: needs, + } +} + +func (h *TestHelper) SetArgs(args ...string) { + h.Args = args +} + +func (h *TestHelper) Execute(s *TestSuite, t *testing.T) { + h.Fn(s, t, h.Args) +} diff --git a/test_job.go b/test_job.go new file mode 100644 index 0000000000000000000000000000000000000000..a94540b9226497187d26aaa1d7a45d0e7751de49 --- /dev/null +++ b/test_job.go @@ -0,0 +1,31 @@ +package snok + +type TestJob struct { + hash int64 + name string + needed bool +} + +func NewTestJob(name string) *TestJob { + return &TestJob{ + hash: hash(name), + name: name, + needed: false, + } +} + +func (j *TestJob) ID() int64 { + return j.hash +} + +func (j *TestJob) Name() string { + return j.name +} + +func (j *TestJob) String() string { + return j.Name() +} + +func (j *TestJob) SetNeeded(needed bool) { + j.needed = needed +} diff --git a/test_suite.go b/test_suite.go new file mode 100644 index 0000000000000000000000000000000000000000..51f9437cb226cf52598b95d95c6d6e81b449592c --- /dev/null +++ b/test_suite.go @@ -0,0 +1,172 @@ +package snok + +import ( + "bytes" + "flag" + "fmt" + "io" + "os" + "regexp" + "strings" + "sync" + "testing" + "unicode" + + "github.com/spf13/cobra" + "github.com/stretchr/testify/require" +) + +type TestSuite struct { + t *testing.T + RootCmd *cobra.Command + LogLevel LogLevel + Graph *TestGraph + unexpectedErr func(require.TestingT, error, ...interface{}) +} + +func NewTestSuite(cmd *cobra.Command) *TestSuite { + s := &TestSuite{ + RootCmd: cmd, + LogLevel: InfoLevel, + Graph: NewTestGraph(), + unexpectedErr: require.NoError, + } + var l string + flag.StringVar(&l, "l", "info", "Log level (error, warn, info, debug, trace)") + flag.Parse() + if os.Getenv("SNOK_LOG_LEVEL") != "" { + l = os.Getenv("SNOK_LOG_LEVEL") + } + lvl, err := ParseLogLevel(l) + if err != nil { + fmt.Println(err) + } + s.LogLevel = lvl + return s +} + +func (s *TestSuite) GetLogLevel() LogLevel { + return s.LogLevel +} + +func (s *TestSuite) Setenv(key, value string) { + s.t.Setenv(key, value) +} + +func (s *TestSuite) GetTest(name string) (*TestCommand, error) { + test, err := s.Graph.GetJob(name) + if err != nil { + return nil, err + } + if testCmd, ok := test.(*TestCommand); ok { + return testCmd, nil + } + return nil, fmt.Errorf("%s is not a test", name) +} + +func (s *TestSuite) AddHelper(name string, fn TestHelperFn, needs ...string) *TestHelper { + helper := NewTestHelper(name, fn, needs...) + s.Graph.AddJob(helper) + return helper +} + +func (s *TestSuite) AddHelperCleanup(f func()) { + s.t.Cleanup(f) +} + +func (s *TestSuite) addTests(t *testing.T, cmd *cobra.Command, cmds []string) { + cmds = append(cmds, cmd.Name()) + annotation, exists := cmd.Annotations["tests"] + if exists { + cmdTests, err := FromAnnotation(annotation) + require.NoError(t, err, "Malformed annotation: %s", annotation) + for _, cmdTest := range cmdTests { + cmdTest.SetCmd(cmd) + cmdTest.SetCmds(cmds) + s.Graph.AddJob(cmdTest) + } + } + if cmd.HasSubCommands() { + for _, cmd := range cmd.Commands() { + s.addTests(t, cmd, cmds) + } + } +} + +func (s *TestSuite) Run(t *testing.T) { + s.t = t + s.addTests(t, s.RootCmd, []string{}) + err := s.Graph.ResolveDeps() + require.NoError(t, err) + jobs, err := s.Graph.SortJobs() + require.NoError(t, err) + for _, job := range jobs { + switch job := job.(type) { + case *TestHelper: + t.Run(fmt.Sprintf("[%s]", job.Name()), func(th *testing.T) { + job.Execute(s, th) + }) + case *TestCommand: + s.executeTest(t, job) + } + } +} + +func (s *TestSuite) executeTest(t *testing.T, test *TestCommand) { + t.Run(test.Name(), func(t *testing.T) { + var input io.Reader + if test.Input != nil { + input = openURI(t, *test.Input) + defer func() { + if r, ok := input.(io.Closer); ok { + r.Close() + } + }() + } + output, err := s.executeCmd(append(test.Cmds[1:], test.Args...), input) + resetCmdFlags(test.Cmd) + if test.ExpectError { + require.Error(t, err, "Expected error") + } else if err != nil { + s.unexpectedErr(t, err, "Unexpected error") + } else if test.Output != nil { + output = strings.TrimRightFunc(output, unicode.IsSpace) + if strings.HasPrefix(*test.Output, "/") && strings.HasSuffix(*test.Output, "/") { + expr := (*test.Output)[1 : len(*test.Output)-1] + re := regexp.MustCompile(expr) + require.True(t, re.MatchString(output), "Expected output to match /%s/, got '%s'", expr, output) + } else { + require.Equal(t, *test.Output, output, "Expected output '%s', got '%s'", *test.Output, output) + } + } + }) +} + +func (s *TestSuite) executeCmd(args []string, input io.Reader) (string, error) { + + osStdout := os.Stdout // keep backup of the real stdout + defer func() { os.Stdout = osStdout }() // restore the real stdout + r, w, _ := os.Pipe() + os.Stdout = w + + output := new(bytes.Buffer) + + var wg sync.WaitGroup + wg.Add(1) + go func() { + _, err := io.Copy(output, r) + require.NoError(s.t, err, "IO Error") + wg.Done() + }() + + s.RootCmd.SetIn(input) + s.RootCmd.SetOutput(w) + s.RootCmd.SetArgs(args) + err := s.RootCmd.Execute() + + w.Close() + wg.Wait() + + return output.String(), err + +} diff --git a/types.go b/types.go deleted file mode 100644 index 40c2f5cf3b628c75736c93a5ad88a7462ca9c088..0000000000000000000000000000000000000000 --- a/types.go +++ /dev/null @@ -1,96 +0,0 @@ -package snok - -import ( - "hash/fnv" - "testing" - - "github.com/spf13/cobra" - "github.com/testcontainers/testcontainers-go" - "gonum.org/v1/gonum/graph" -) - -// hash calculates the hash value of a string using the FNV-1a algorithm. -func hash(s string) int64 { - hash := fnv.New64a() - hash.Write([]byte(s)) - return int64(hash.Sum64()) -} - -// Node is a graph node. It returns a graph-unique integer ID and a list of -// integer IDs of nodes that it depends on. -type Node interface { - graph.Node - // Needed sets the needed flag to true or false. - SetNeeded(bool) - // DependencyIDs returns a list of graph-unique integer ID for each dependency. - DependencyIDs() []int64 -} - -// node in the graph. helper and test inherit from this. -type node struct { - id *int64 - Name string `json:"name,omitempty"` - Needs []string `json:"needs,omitempty"` - needed bool -} - -func (n *node) ID() int64 { - if n.id == nil { - n.id = new(int64) - *n.id = hash(n.Name) - } - return *n.id -} - -func (n *node) SetNeeded(needed bool) { - n.needed = needed -} - -func (n *node) DependencyIDs() []int64 { - needs := make([]int64, len(n.Needs)) - for i, need := range n.Needs { - needs[i] = hash(need) - } - return needs -} - -func (n *node) String() string { - return n.Name -} - -// helper node in the graph. -type helper struct { - node - fn func(*Tests, *testing.T, []string) - args []string -} - -func (h *helper) SetArgs(args ...string) { - h.args = args -} - -// test node in the graph. -type test struct { - node - cmd *cobra.Command - cmds []string - Args []string `json:"args,omitempty"` - Input *string `json:"input,omitempty"` - Output *string `json:"output,omitempty"` - ExpectError bool `json:"expectError,omitempty"` -} - -// TestLogConsumer consumes testcontainer logs. -type TestLogConsumer struct { - t *testing.T - testcontainers.LogConsumer -} - -func NewTestLogConsumer(t *testing.T) *TestLogConsumer { - return &TestLogConsumer{t: t} -} - -func (l *TestLogConsumer) Accept(log testcontainers.Log) { - l.t.Helper() - l.t.Logf("%s", log.Content) -} diff --git a/uri.go b/utils.go similarity index 74% rename from uri.go rename to utils.go index 9139e367ddb68c1f96d4b3a2d1cddb48c0d336d5..32c77860f62281e2915584343a94770e3e374f5a 100644 --- a/uri.go +++ b/utils.go @@ -3,6 +3,7 @@ package snok import ( "bytes" "encoding/base64" + "hash/fnv" "io" "net/http" "net/url" @@ -10,9 +11,27 @@ import ( "strings" "testing" + "github.com/spf13/cobra" + "github.com/spf13/pflag" "github.com/stretchr/testify/require" ) +// hash calculates the hash value of a string using the FNV-1a algorithm. +func hash(s string) int64 { + hash := fnv.New64a() + hash.Write([]byte(s)) + return int64(hash.Sum64()) +} + +func resetCmdFlags(cmd *cobra.Command) { + cmd.Flags().VisitAll(func(pf *pflag.Flag) { + if pf.Changed { + _ = pf.Value.Set(pf.DefValue) + pf.Changed = false + } + }) +} + func openURI(t *testing.T, uri string) io.Reader { parsedURL, err := url.Parse(uri) require.NoError(t, err)