diff --git a/integration/instance.go b/integration/instance.go new file mode 100644 index 000000000..617a21b75 --- /dev/null +++ b/integration/instance.go @@ -0,0 +1,439 @@ +// Mgmt +// Copyright (C) 2013-2018+ James Shubin and the project contributors +// Written by James Shubin and the project contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +// notes: +// https://www.philosophicalhacker.com/post/integration-tests-in-go/ +// https://github.com/golang/go/wiki/TableDrivenTests +// https://blog.golang.org/subtests +// https://splice.com/blog/lesser-known-features-go-test/ +// https://coreos.com/blog/testing-distributed-systems-in-go.html +// https://www.philosophicalhacker.com/post/integration-tests-in-go/ + +package integration + +import ( + "bytes" + "context" + "fmt" + "io/ioutil" + "os" + "os/exec" + "path" + "path/filepath" + "runtime" + "strings" + "syscall" + "testing" + "time" + + errwrap "github.com/pkg/errors" +) + +const ( + defaultTimeout = 120 * time.Second // longest time a mgmt instance should exist during tests + convergedTimeout = 5 * time.Second + idleTimeout = 5 * time.Second + shutdownTimeout = 10 * time.Second + convergedIndicator = "Converged for 5 seconds, exiting!" + exitedIndicator = "Goodbye!" +) + +// Instance represents a single mgmt instance that can be managed +type Instance struct { + // settings passed into the instance + Timeout time.Duration + Env []string + + // output and metadata regarding the mgmt instance + Name string + Tmpdir string + Prefix string + Workdir string + DeployOutput string + Stdout bytes.Buffer + Stderr bytes.Buffer + Err error + Seeds string + + command string + env string + ctx context.Context + cmd *exec.Cmd + cancel context.CancelFunc +} + +// start starts running mgmt run in an isolated environment +func (m *Instance) start(t *testing.T, mgmtargs ...string) error { + // TODO: fakechroot/docker for proper isolation, for now relying on passing a temp. dir + // TODO: currently better isolation for used client/server ports is much needed + + if m.Timeout == 0 { + m.Timeout = defaultTimeout + } + + m.ctx, m.cancel = context.WithTimeout(context.Background(), m.Timeout) + + if m.Tmpdir == "" { + // create temporary directory to use during testing + var err error + m.Tmpdir, err = ioutil.TempDir("", fmt.Sprintf("mgmt-integrationtest-%s-", m.Name)) + if err != nil { + if t != nil { + t.Helper() + t.Fatal("Error: can't create temporary directory") + } + return err + } + } + + prefix := path.Join(m.Tmpdir, "prefix") + if err := os.MkdirAll(prefix, 0755); err != nil { + if t != nil { + t.Helper() + t.Fatal("Error: can't create temporary prefix directory") + } + return err + } + m.Prefix = prefix + workdir := path.Join(m.Tmpdir, "workdir") + if err := os.MkdirAll(workdir, 0755); err != nil { + if t != nil { + t.Helper() + t.Fatal("Error: can't create temporary working directory") + } + return err + } + m.Workdir = workdir + + cmdargs := []string{"run"} + if m.Name != "" { + cmdargs = append(cmdargs, fmt.Sprintf("--hostname=%s", m.Name)) + } + cmdargs = append(cmdargs, mgmtargs...) + m.command = fmt.Sprintf("%s %s", mgmt, strings.Join(cmdargs, " ")) + + m.cmd = exec.CommandContext(m.ctx, mgmt, cmdargs...) + + m.cmd.Stdout = &m.Stdout + m.cmd.Stderr = &m.Stderr + + m.cmd.Env = []string{ + fmt.Sprintf("MGMT_TEST_ROOT=%s", workdir), + fmt.Sprintf("MGMT_PREFIX=%s", prefix), + } + m.cmd.Env = append(m.cmd.Env, m.Env...) + m.env = strings.Join(m.cmd.Env, " ") + + m.Seeds = fmt.Sprintf("http://127.0.0.1:2379") + + if err := m.cmd.Start(); err != nil { + if t != nil { + t.Helper() + t.Fatal(errwrap.Wrapf(err, "Command %s failed to start", m.command)) + } + return err + } + return nil +} + +// wait waits for previously started mgmt run to finish cleanly +func (m *Instance) wait(t *testing.T) error { + if err := m.cmd.Wait(); err != nil { + if t != nil { + t.Helper() + t.Fatal(errwrap.Wrapf(err, "Command failed to complete")) + } + return err + } + return nil +} + +// stop stops the mgmt background instance +func (m *Instance) stop(t *testing.T) error { + if err := m.cmd.Process.Signal(syscall.SIGINT); err != nil { + if t != nil { + t.Helper() + t.Fatal(errwrap.Wrapf(err, "Failed to kill the command")) + } + return err + } + + if err := m.cmd.Wait(); err != nil { + // kill the process if it fails to shutdown nicely, so we get a stack trace + m.cmd.Process.Signal(syscall.SIGKILL) + if t != nil { + t.Helper() + t.Fatal(errwrap.Wrapf(err, "Command '%s' failed to complete", m.command)) + } + return err + } + + if m.ctx.Err() == context.DeadlineExceeded { + if t != nil { + t.Helper() + t.Fatal("Command timed out") + } + return fmt.Errorf("Command timed out") + } + return nil +} + +// Run runs mgmt to convergence +func (m *Instance) Run(t *testing.T, mgmtargs ...string) error { + mgmtargs = append(mgmtargs, "--converged-timeout=5") + if err := m.start(t, mgmtargs...); err != nil { + return err + } + if err := m.wait(t); err != nil { + return err + } + return nil +} + +// RunLang runs mgmt with the given mcl file to convergence +func (m *Instance) RunLang(t *testing.T, langfilerelpath string, mgmtargs ...string) error { + _, testfilepath, _, _ := runtime.Caller(0) + testdirpath := filepath.Dir(testfilepath) + langfilepath := path.Join(testdirpath, langfilerelpath) + + mgmtargs = append(mgmtargs, fmt.Sprintf("--lang=%s", langfilepath)) + + return m.Run(t, mgmtargs...) +} + +// RunBackground runs mgmt in the background +func (m *Instance) RunBackground(t *testing.T, mgmtargs ...string) error { + return m.start(t, mgmtargs...) +} + +// DeployLang deploys a mcl file provided as content to the running instance +func (m *Instance) DeployLang(t *testing.T, code string) (string, error) { + content := []byte(code) + tmpfile, err := ioutil.TempFile("", "deploy.mcl") + if err != nil { + if t != nil { + t.Helper() + t.Fatal(err) + } + return "", err + } + + defer os.Remove(tmpfile.Name()) // clean up + + if _, err := tmpfile.Write(content); err != nil { + if t != nil { + t.Helper() + t.Fatal(err) + } + return "", err + } + if err := tmpfile.Close(); err != nil { + if t != nil { + t.Helper() + t.Fatal(err) + } + return "", err + } + + cmd := exec.Command(mgmt, "deploy", "--no-git", "lang", "--lang", tmpfile.Name()) + // TODO: environment should be shared by instance and deploy + cmd.Env = []string{fmt.Sprintf("MGMT_SEEDS=%s", m.Seeds)} + + out, err := cmd.CombinedOutput() + if err != nil { + msg := errwrap.Wrapf(err, "Deploy failed") + if t != nil { + t.Logf("Deploy output: %s", out) + t.Helper() + t.Fatal(msg) + } + return string(out), msg + } + m.DeployOutput = string(out) + + return string(out), nil +} + +// DeployLangFile deploys a mcl file to the running instance +func (m *Instance) DeployLangFile(t *testing.T, langfilerelpath string) (string, error) { + _, testfilepath, _, _ := runtime.Caller(0) + testdirpath := filepath.Dir(testfilepath) + langfilepath := path.Join(testdirpath, langfilerelpath) + + cmd := exec.Command(mgmt, "deploy", "--no-git", "lang", "--lang", langfilepath) + // TODO: environment should be shared by instance and deploy + cmd.Env = []string{fmt.Sprintf("MGMT_SEEDS=%s", m.Seeds)} + + out, err := cmd.CombinedOutput() + if err != nil { + msg := errwrap.Wrapf(err, "Deploy failed") + if t != nil { + t.Logf("Deploy output: %s", out) + t.Helper() + t.Fatal(msg) + } + return string(out), msg + } + m.DeployOutput = string(out) + + return string(out), nil +} + +// StopBackground stops the mgmt instance running in the background +func (m *Instance) StopBackground(t *testing.T) error { + t.Helper() + err := m.stop(t) + + // TODO: proper shutdown checking, eg "Main: Waiting..." is last logline for x seconds + time.Sleep(shutdownTimeout) + + return err +} + +// Finished indicates if the command output matches that of a converged and finished (exited) run +func (m Instance) Finished(t *testing.T, converge bool) error { + if m.Stderr.String() == "" { + if t != nil { + t.Helper() + t.Fatal("Instance run had no output") + } + return fmt.Errorf("Instance run had no output") + } + + var converged bool + if converge { + converged = strings.Contains(m.Stderr.String(), convergedIndicator) + } else { + converged = true + } + exited := strings.Contains(m.Stderr.String(), exitedIndicator) + if !(converged && exited) { + t.Logf("Command output: %s", m.Stderr.String()) + if t != nil { + t.Helper() + t.Fatal("Instance run output does not indicate finished run") + } + return fmt.Errorf("Instance run output does not indicate finished run") + } + return nil +} + +// Pass checks if a non-empty `pass` file exists in the workdir +// This file should be created by the configuration run to indicate it completed. +func (m Instance) Pass(t *testing.T) error { + passfilepath := path.Join(m.Workdir, "pass") + passfilestat, err := os.Stat(passfilepath) + if os.IsNotExist(err) { + msg := fmt.Sprintf("the file `%s` was not created by the configuration", passfilepath) + if t != nil { + t.Helper() + t.Fatal(msg) + } + return fmt.Errorf(msg) + } + if passfilestat.Size() == 0 { + msg := fmt.Sprintf("the file `%s` is empty", passfilepath) + if t != nil { + t.Helper() + t.Fatal(msg) + } + return fmt.Errorf(msg) + } + return nil +} + +// Cleanup makes sure temporary directory is cleaned +func (m *Instance) Cleanup(t *testing.T) error { + // stop the timeout context for the command + m.cancel() + // we expect the command to be stopped (StopBackground()) or else assume we don't care about it closing nicely + m.cmd.Process.Signal(syscall.SIGKILL) + + // be helpful on failure and keep temporary directories for debugging + if t.Failed() { + t.Logf("\nName: %s\nRan command:\nenv %s %s\nStdout:\n%s\nStderr:\n%s", + m.Name, m.env, m.command, m.Stdout.String(), m.Stderr.String()) + if m.DeployOutput != "" { + t.Logf("Deploy output:\n%s", m.DeployOutput) + } + return nil + } + + if m.Tmpdir == "" { + return nil + } + if err := os.RemoveAll(m.Tmpdir); err != nil { + return err + } + m.Tmpdir = "" + return nil +} + +// WaitUntilIdle waits for the current instance to reach reach idle state () +func (m *Instance) WaitUntilIdle(t *testing.T) error { + // TODO: sleep is bad UX on testing, refactor to wait for a signal either from logging or maybe using etcdclient and AddHostnameConvergedWatcher? + // TODO: should we consider being idle the same as being converged? + time.Sleep(idleTimeout) + + return nil +} + +// WaitUntilConverged waits for the current instance to reach a converged state +func (m *Instance) WaitUntilConverged(t *testing.T) error { + // TODO: sleep is bad UX on testing, refactor to wait for a signal either from logging or maybe using etcdclient and AddHostnameConvergedWatcher? + time.Sleep(convergedTimeout) + + return nil +} + +// WorkdirWriteToFile write a string to a file in the current instance workdir +func (m *Instance) WorkdirWriteToFile(t *testing.T, name string, text string) error { + data := []byte(text) + if err := ioutil.WriteFile(path.Join(m.Workdir, name), data, 0644); err != nil { + if t != nil { + t.Helper() + t.Fatal(errwrap.Wrapf(err, "failed to create file %s in mgmt instance working directory", name)) + } + return err + } + return nil +} + +// WorkdirReadFromFile reads a file from the workdir and returns the content as a string +func (m *Instance) WorkdirReadFromFile(t *testing.T, name string) (string, error) { + data, err := ioutil.ReadFile(path.Join(m.Workdir, name)) + if err != nil { + if t != nil { + t.Helper() + t.Fatal(errwrap.Wrapf(err, "failed to read file %s from workdir", name)) + } + return "", err + } + return string(data), nil +} + +// WorkdirRemoveFile removes a file from the instance workdir +func (m *Instance) WorkdirRemoveFile(t *testing.T, name string) error { + if err := os.Remove(path.Join(m.Workdir, name)); err != nil { + if t != nil { + t.Helper() + t.Fatal(errwrap.Wrapf(err, "failed to remove file %s in mgmt instance working directory", name)) + } + return err + } + return nil +} diff --git a/integration/lang/simple.mcl b/integration/lang/simple.mcl new file mode 100644 index 000000000..a7dfe1d7b --- /dev/null +++ b/integration/lang/simple.mcl @@ -0,0 +1,6 @@ +# minimal example to make integrationtest pass +$root = getenv("MGMT_TEST_ROOT") + +file "${root}/pass" { + content => "not empty", +} diff --git a/integration/sanity_test.go b/integration/sanity_test.go new file mode 100644 index 000000000..fbc1886ee --- /dev/null +++ b/integration/sanity_test.go @@ -0,0 +1,112 @@ +// Mgmt +// Copyright (C) 2013-2018+ James Shubin and the project contributors +// Written by James Shubin and the project contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package integration + +import ( + "net/http" + "os/exec" + "strings" + "testing" + + errwrap "github.com/pkg/errors" +) + +const ( + stringExistingInHelp = "--version, -v print the version" +) + +func TestHelp(t *testing.T) { + out, err := exec.Command(mgmt).Output() + if err != nil { + t.Fatalf("Error: %v", err) + } + if !strings.Contains(string(out), stringExistingInHelp) { + t.Logf("Command output: %s", string(out)) + t.Fatal("Expected output not found") + } +} + +// TestSmoketest makes sure the most basic functionality works. +// If this test fails assumptions made by the rest of the testsuite are invalid. +func TestSmoketest(t *testing.T) { + // create an mgmt test environment and ensure cleanup/debug logging on failure/exit + m := Instance{} + defer m.Cleanup(t) + + // run mgmt to convergence + m.Run(t) + + // verify output contains what is expected from a converging and finished run + m.Finished(t, true) +} + +// TestSimple applies a simple mcl file and tests the result. +// If this test fails assumptions made by the rest of the testsuite are invalid. +func TestSimple(t *testing.T) { + // create an mgmt test environment and ensure cleanup/debug logging on failure/exit + m := Instance{} + defer m.Cleanup(t) + + // apply the configration from lang/simple.mcl + m.RunLang(t, "lang/simple.mcl") + + // verify output contains what is expected from a converging and finished run + m.Finished(t, true) + + // verify if a non-empty `pass` file is created in the working directory + m.Pass(t) +} + +// TestDeploy checks if background running and deployment works. +// If this test fails assumptions made by the rest of the testsuite are invalid. +func TestDeploy(t *testing.T) { + // create an mgmt test environment and ensure cleanup/debug logging on failure/exit + m := Instance{} + defer m.Cleanup(t) + + // start a mgmt instance running in the background + m.RunBackground(t) + + // wait until server is up and running + m.WaitUntilIdle(t) + + // expect mgmt to listen on default client and server url + if _, err := http.Get("http://127.0.0.1:2379"); err != nil { + t.Fatal("default client url is not reachable over tcp") + } + if _, err := http.Get("http://127.0.0.1:2380"); err != nil { + t.Fatal("default server url is not reachable over tcp") + } + + // deploy lang file to the just started instance + out, err := m.DeployLangFile(nil, "lang/simple.mcl") + if err != nil { + t.Fatal(errwrap.Wrapf(err, "deploy command failed, output: %s", out)) + } + // wait for deploy to come to a rest + m.WaitUntilConverged(t) + + // stop the running instance + m.StopBackground(t) + + // verify output contains what is expected from a converged and cleanly finished run + m.Finished(t, false) + + // verify if a non-empty `pass` file is created in the working directory + m.Pass(t) +} diff --git a/integration/shared_test.go b/integration/shared_test.go new file mode 100644 index 000000000..510a36f23 --- /dev/null +++ b/integration/shared_test.go @@ -0,0 +1,74 @@ +// Mgmt +// Copyright (C) 2013-2018+ James Shubin and the project contributors +// Written by James Shubin and the project contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package integration + +import ( + "io/ioutil" + "log" + "os" + "path/filepath" + "testing" +) + +var mgmt string + +// TestMain ensures a temporary environment is created (and cleaned afterward) to perform tests in +func TestMain(m *testing.M) { + // get absolute path for mgmt binary from testenvironment + mgmt = os.Getenv("MGMT") + // fallback to assumption based on current directory if path is not provided + if mgmt == "" { + path, err := filepath.Abs("../mgmt") + if err != nil { + log.Printf("failed to get absolute mgmt path") + os.Exit(1) + } + mgmt = path + } + if _, err := os.Stat(mgmt); os.IsNotExist(err) { + log.Printf("mgmt executable %s does not exist", mgmt) + os.Exit(1) + } + + // move to clean/stateless directory before running tests + cwd, err := os.Getwd() + if err != nil { + log.Printf("failed to get current directory") + os.Exit(1) + } + tmpdir, err := ioutil.TempDir("", "mgmt-integrationtest") + if err != nil { + log.Printf("failed to create test working directory") + os.Exit(1) + } + if err := os.Chdir(tmpdir); err != nil { + log.Printf("failed to enter test working directory") + os.Exit(1) + } + + // run all the tests + os.Exit(m.Run()) + + // and back to where we started + os.Chdir(cwd) + + if err := os.RemoveAll(tmpdir); err != nil { + log.Printf("failed to remove working directory") + os.Exit(1) + } +} diff --git a/lib/cli.go b/lib/cli.go index 9c0a4fc65..ae3c81a17 100644 --- a/lib/cli.go +++ b/lib/cli.go @@ -457,6 +457,39 @@ func CLI(program, version string, flags Flags) error { }, }, }, + { + Name: "info", + Aliases: []string{"i"}, + Usage: "info", + Action: info, + // Flags: , + Subcommands: []cli.Command{ + { + Name: "resources", + Aliases: []string{"res", "r"}, + Usage: "list available resources", + Action: info, + Flags: []cli.Flag{ + cli.BoolFlag{ + Name: "type, t", + Usage: "also show type info", + }, + }, + }, + { + Name: "functions", + Aliases: []string{"funcs", "f"}, + Usage: "list available functions", + Action: info, + Flags: []cli.Flag{ + cli.BoolFlag{ + Name: "type, t", + Usage: "also show type info", + }, + }, + }, + }, + }, } app.EnableBashCompletion = true return app.Run(os.Args) diff --git a/lib/info.go b/lib/info.go new file mode 100644 index 000000000..39134a270 --- /dev/null +++ b/lib/info.go @@ -0,0 +1,127 @@ +// Mgmt +// Copyright (C) 2013-2018+ James Shubin and the project contributors +// Written by James Shubin and the project contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package lib + +import ( + "bytes" + "fmt" + "os" + "reflect" + "sort" + "strings" + "text/tabwriter" + + "github.com/purpleidea/mgmt/lang/funcs" + "github.com/purpleidea/mgmt/lang/interfaces" + "github.com/purpleidea/mgmt/resources" + "github.com/urfave/cli" +) + +const ( + twMinWidth = 0 + twTabWidth = 8 + twPadding = 2 // ensure columns have at least a space between them + twPadChar = ' ' // using a tab here creates 'jumpy' columns on output + twFlags = 0 +) + +// info wraps infocmd to produce output to stdout +func info(c *cli.Context) error { + output, err := infoCmd(c) + if err != nil { + return err + } + w := tabwriter.NewWriter(os.Stdout, twMinWidth, twTabWidth, twPadding, twPadChar, twFlags) + fmt.Fprint(w, output.String()) + w.Flush() + return nil +} + +// infoCmd takes the cli context and returns the requested output +func infoCmd(c *cli.Context) (bytes.Buffer, error) { + var out bytes.Buffer + var names []string + descriptions := make(map[string]string) + + switch c.Command.Name { + case "resources": + for _, name := range resources.RegisteredResourcesNames() { + names = append(names, name) + + descriptions[name] = "" + + res, err := resources.Lookup(name) + if err != nil { + continue + } + + s := reflect.ValueOf(res).Elem() + typeOfT := s.Type() + + var fields []string + for i := 0; i < s.NumField(); i++ { + field := typeOfT.Field(i) + // skip unexported fields + if field.PkgPath != "" { + continue + } + fieldname := field.Name + if fieldname == "BaseRes" { + continue + } + f := s.Field(i) + fieldtype := f.Type() + fields = append(fields, fmt.Sprintf("%s (%s)", strings.ToLower(fieldname), fieldtype)) + } + descriptions[name] = strings.Join(fields, ", ") + + } + case "functions": + for name := range funcs.RegisteredFuncs { + // skip internal functions (history, operations, etc) + if strings.HasPrefix(name, "_") { + continue + } + names = append(names, name) + descriptions[name] = "" + fn, err := funcs.Lookup(name) + if err != nil { + continue + } + if _, ok := fn.(interfaces.PolyFunc); !ok { + // TODO: skip for now, needs Build before Info + continue + } + + // set function signature as description + descriptions[name] = strings.Replace(fn.Info().Sig.String(), "func", name, 1) + } + default: + return out, fmt.Errorf("invalid command") + } + + sort.Strings(names) + for _, name := range names { + if c.Bool("type") { + fmt.Fprintf(&out, "%s\t%s\n", name, descriptions[name]) + } else { + fmt.Fprintln(&out, name) + } + } + return out, nil +} diff --git a/lib/info_test.go b/lib/info_test.go new file mode 100644 index 000000000..818d24485 --- /dev/null +++ b/lib/info_test.go @@ -0,0 +1,65 @@ +// Mgmt +// Copyright (C) 2013-2018+ James Shubin and the project contributors +// Written by James Shubin and the project contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package lib + +import ( + "flag" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/urfave/cli" +) + +func TestResources(t *testing.T) { + set := flag.NewFlagSet("test", 0) + ctx := cli.NewContext(nil, set, nil) + ctx.Command = cli.Command{Name: "resources"} + + out, err := infoCmd(ctx) + if err != nil { + t.Fatal("failed") + } + assert.Contains(t, out.String(), "file") +} + +func TestFunctionsWithTypes(t *testing.T) { + set := flag.NewFlagSet("test", 0) + set.Bool("type", true, "doc") + ctx := cli.NewContext(nil, set, nil) + ctx.Command = cli.Command{Name: "functions"} + + out, err := infoCmd(ctx) + if err != nil { + t.Fatal("failed") + } + assert.Contains(t, out.String(), "load() struct{x1 float; x5 float; x15 float}") +} + +// TODO: see infoCmd(), this still needs some work +// func TestPolyFunctionsWithTypes(t *testing.T) { +// set := flag.NewFlagSet("test", 0) +// set.Bool("type", true, "doc") +// ctx := cli.NewContext(nil, set, nil) +// ctx.Command = cli.Command{Name: "functions"} +// +// out, err := infoCmd(ctx) +// if err != nil { +// t.Fatal("failed") +// } +// assert.Contains(t, out.String(), "somethingmore") +// } diff --git a/resources/resources.go b/resources/resources.go index b60b46174..6aa75dabc 100644 --- a/resources/resources.go +++ b/resources/resources.go @@ -673,3 +673,12 @@ func VtoR(v pgraph.Vertex) Res { // } // return nil //} + +// Lookup returns a pointer to the resource's struct. +func Lookup(name string) (Res, error) { + r, exists := registeredResources[name] + if !exists { + return nil, fmt.Errorf("not found") + } + return r(), nil +} diff --git a/test.sh b/test.sh index 8c796e8b9..2e93ab013 100755 --- a/test.sh +++ b/test.sh @@ -53,6 +53,7 @@ run-testsuite ./test/test-commit-message.sh run-testsuite ./test/test-govet.sh run-testsuite ./test/test-examples.sh run-testsuite ./test/test-gotest.sh +run-testsuite ./test/test-integration.sh # skipping: https://github.com/purpleidea/mgmt/issues/327 # run-test ./test/test-crossbuild.sh diff --git a/test/test-gotest.sh b/test/test-gotest.sh index 17cad7e75..c58cd2b67 100755 --- a/test/test-gotest.sh +++ b/test/test-gotest.sh @@ -14,14 +14,19 @@ function run-test() } base=$(go list .) -for pkg in `go list -e ./... | grep -v "^${base}/vendor/" | grep -v "^${base}/examples/" | grep -v "^${base}/test/" | grep -v "^${base}/old/" | grep -v "^${base}/tmp/"`; do +for pkg in `go list -e ./... | grep -v "^${base}/vendor/" | grep -v "^${base}/examples/" | grep -v "^${base}/test/" | grep -v "^${base}/old/" | grep -v "^${base}/tmp/" | grep -v "^${base}/integration/"`; do echo -e "\ttesting: $pkg" run-test go test "$pkg" if [ "$1" = "--race" ]; then + shift run-test go test -race "$pkg" fi done +if [[ "$@" = *"--integration"* ]]; then + run-test go test github.com/purpleidea/mgmt/integration/ +fi + if [[ -n "$failures" ]]; then echo 'FAIL' echo 'The following `go test` runs have failed:' diff --git a/test/test-integration.sh b/test/test-integration.sh new file mode 100755 index 000000000..f534951d3 --- /dev/null +++ b/test/test-integration.sh @@ -0,0 +1,25 @@ +#!/bin/bash + +echo running "$0" "$@" + +#ROOT="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && cd .. && pwd )" # dir! +ROOT=$(dirname "${BASH_SOURCE}")/.. +cd "${ROOT}" +. test/util.sh + +failures='' +function run-test() +{ + $@ || failures=$( [ -n "$failures" ] && echo "$failures\\n$@" || echo "$@" ) +} + +run-test go test github.com/purpleidea/mgmt/integration/ + +if [[ -n "$failures" ]]; then + echo 'FAIL' + echo 'The following `go test` runs have failed:' + echo -e "$failures" + echo + exit 1 +fi +echo 'PASS'