// Mgmt
// Copyright (C) James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> 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 <https://www.gnu.org/licenses/>.
//
// Additional permission under GNU GPL version 3 section 7
//
// If you modify this program, or any covered work, by linking or combining it
// with embedded mcl code and modules (and that the embedded mcl code and
// modules which link with this program, contain a copy of their source code in
// the authoritative form) containing parts covered by the terms of any other
// license, the licensors of this program grant you additional permission to
// convey the resulting work. Furthermore, the licensors of this program grant
// the original author, James Shubin, additional permission to update this
// additional permission if he deems it necessary to achieve the goals of this
// additional permission.

//go:build !root

package resources

import (
	"context"
	"fmt"
	"os"
	"os/user"
	"path"
	"path/filepath"
	"strconv"
	"strings"
	"sync"
	"syscall"
	"testing"
	"time"

	"github.com/purpleidea/mgmt/engine"
	engineUtil "github.com/purpleidea/mgmt/engine/util"
	"github.com/purpleidea/mgmt/pgraph"
	"github.com/purpleidea/mgmt/util"
	"github.com/purpleidea/mgmt/util/errwrap"
)

// TODO: consider providing this as a lib so that we can add tests into the
// specific _test.go file of each resource.

// makeRes is a helper function to build a res. It should only be called in
// tests, because it panics if something goes wrong.
func makeRes(kind, name string) engine.Res {
	res, err := engine.NewNamedResource(kind, name)
	if err != nil {
		panic(fmt.Sprintf("could not create resource: %+v", err))
	}
	return res
}

// Step is used for the timeline in tests.
type Step interface {
	Action() error
	Expect() error
}

type manualStep struct {
	action func() error
	expect func() error
}

func (obj *manualStep) Action() error {
	return obj.action()
}
func (obj *manualStep) Expect() error {
	return obj.expect()
}

// NewManualStep creates a new manual step with an action and an expect test.
func NewManualStep(action, expect func() error) Step {
	return &manualStep{
		action: action,
		expect: expect,
	}
}

type startupStep struct {
	ms uint
	ch chan struct{} // set by test harness
}

func (obj *startupStep) Action() error {
	select {
	case <-obj.ch: // called by Event() in Watch
	//nolint:gosec // G115: ms is a test-controlled value
	case <-time.After(time.Duration(obj.ms) * time.Millisecond):
		return fmt.Errorf("took too long to startup")
	}
	return nil
}
func (obj *startupStep) Expect() error { return nil }

// NewStartupStep waits up to this many ms for the Watch function to startup.
func NewStartupStep(ms uint) Step {
	return &startupStep{
		ms: ms,
	}
}

type changedStep struct {
	ms     uint
	expect bool      // what checkOK value we're expecting
	ch     chan bool // set by test harness, filled with checkOK values
}

func (obj *changedStep) Action() error {
	select {
	case checkOK, ok := <-obj.ch: // from CheckApply() in test Process loop
		if !ok {
			return fmt.Errorf("channel closed unexpectedly")
		}
		if checkOK != obj.expect {
			return fmt.Errorf("got unexpected checkOK value of: %t", checkOK)
		}
	//nolint:gosec // G115: ms is a test-controlled value
	case <-time.After(time.Duration(obj.ms) * time.Millisecond):
		return fmt.Errorf("took too long to startup")
	}
	return nil
}
func (obj *changedStep) Expect() error { return nil }

// NewChangedStep waits up to this many ms for a CheckApply action to occur.
// Watch function to startup.
func NewChangedStep(ms uint, expect bool) Step {
	return &changedStep{
		ms:     ms,
		expect: expect,
	}
}

type clearChangedStep struct {
	ms uint
	ch chan bool // set by test harness, filled with checkOK values
}

func (obj *clearChangedStep) Action() error {
	// read all pending events...
	for {
		select {
		case _, ok := <-obj.ch: // from CheckApply() in test Process loop
			if !ok {
				return fmt.Errorf("channel closed unexpectedly")
			}
		//nolint:gosec // G115: ms is a test-controlled value
		case <-time.After(time.Duration(obj.ms) * time.Millisecond):
			return nil // done waiting
		}
	}
}
func (obj *clearChangedStep) Expect() error { return nil }

// NewClearChangedStep waits up to this many ms for additional CheckApply
// actions to occur, and flushes them all so that a future NewChangedStep won't
// see unwanted events.
func NewClearChangedStep(ms uint) Step {
	return &clearChangedStep{
		ms: ms,
	}
}

// FileExpect takes a path and a string to expect in that file, and builds a
// Step that checks that out of them.
func FileExpect(p, s string) Step { // path & string
	return &manualStep{
		action: func() error { return nil },
		expect: func() error {
			content, err := os.ReadFile(p)
			if err != nil {
				return err
			}
			if string(content) != s {
				return fmt.Errorf("contents did not match in %s", p)
			}
			return nil
		},
	}
}

// FileOwnerExpect takes a path and a uid to expect from that file, and builds a
// Step that checks that out of them.
func FileOwnerExpect(p, o string) Step { // path & owner
	return &manualStep{
		action: func() error { return nil },
		expect: func() error {
			fileInfo, err := os.Stat(p)
			if err != nil {
				return err
			}
			stat, ok := fileInfo.Sys().(*syscall.Stat_t)
			if !ok {
				return fmt.Errorf("file stat type did not match in %s", p)
			}
			i, err := strconv.ParseUint(o, 10, 32)
			if err != nil {
				return err
			}
			if i != uint64(stat.Uid) {
				return fmt.Errorf("file uid did not match in %s", p)
			}
			return nil
		},
	}
}

// FileWrite takes a path and a string to write to that file, and builds a Step
// that does that to them.
func FileWrite(p, s string) Step { // path & string
	return &manualStep{
		action: func() error {
			// TODO: apparently using 0666 is equivalent to respecting the current umask
			const umask = 0666
			return os.WriteFile(p, []byte(s), umask)
		},
		expect: func() error { return nil },
	}
}

// ErrIsNotExistOK returns nil if we get an IsNotExist true result on the error.
func ErrIsNotExistOK(e error) error {
	if os.IsNotExist(e) {
		return nil
	}
	return errwrap.Wrapf(e, "unexpected error")
}

// GetUID returns the UID of the user running this test. It consults the process
// identity directly rather than $USER, which may be unset under some test
// runners.
func GetUID() (string, error) {
	u, err := user.Current()
	if err != nil {
		return "", err
	}
	return u.Uid, nil
}

func TestResources1(t *testing.T) {
	type test struct { // an individual test
		name      string
		res       engine.Res // a resource
		fail      bool
		experr    error        // expected error if fail == true (nil ignores it)
		experrstr string       // expected error prefix
		timeline  []Step       // TODO: this could be a generator that keeps pushing out steps until it's done!
		expect    func() error // function to check for expected state
		startup   func() error // function to run as startup
		cleanup   func() error // function to run as cleanup
	}

	// helpers
	// TODO: make a series of helps to orchestrate the resources (eg: edit
	// file, wait for event w/ timeout, run command w/ timeout, etc...)
	sleep := func(ms uint) Step {
		return &manualStep{
			action: func() error {
				//nolint:gosec // G115: ms is a test-controlled value
				time.Sleep(time.Duration(ms) * time.Millisecond)
				return nil
			},
			expect: func() error { return nil },
		}
	}

	testCases := []test{}
	{
		r := makeRes("file", "r1")
		res := r.(*FileRes) // if this panics, the test will panic
		p := filepath.Join(t.TempDir(), "whatever")
		s := "hello, world\n"
		res.Path = p
		res.State = FileStateExists
		contents := s
		res.Content = &contents

		timeline := []Step{
			NewStartupStep(1000 * 60),          // startup
			NewChangedStep(1000*60, false),     // did we do something?
			FileExpect(p, s),                   // check initial state
			NewClearChangedStep(1000 * 15),     // did we do something?
			FileWrite(p, "this is whatever\n"), // change state
			NewChangedStep(1000*60, false),     // did we do something?
			FileExpect(p, s),                   // check again
			sleep(1),                           // we can sleep too!
		}

		testCases = append(testCases, test{
			name:     "simple file",
			res:      res,
			fail:     false,
			timeline: timeline,
			expect:   func() error { return nil },
			startup:  func() error { return nil },
			cleanup:  func() error { return os.Remove(p) },
		})
	}
	{
		r := makeRes("exec", "x1")
		res := r.(*ExecRes) // if this panics, the test will panic
		s := "hello, world"
		f := filepath.Join(t.TempDir(), "whatever")
		res.Cmd = fmt.Sprintf("echo '%s' > '%s'", s, f)
		res.Shell = "/bin/bash"
		res.IfCmd = fmt.Sprintf("! diff <(cat %q) <(echo hello, world)", f)
		res.IfShell = "/bin/bash"
		res.WatchCmd = fmt.Sprintf("/usr/bin/inotifywait -e modify -m %s", f)
		//res.WatchShell = "/bin/bash"

		timeline := []Step{
			NewStartupStep(1000 * 60),        // startup
			NewChangedStep(1000*60, false),   // did we do something?
			FileExpect(f, s+"\n"),            // check initial state
			NewClearChangedStep(1000 * 15),   // did we do something?
			FileWrite(f, "this is stuff!\n"), // change state
			NewChangedStep(1000*60, false),   // did we do something?
			FileExpect(f, s+"\n"),            // check again
			sleep(1),                         // we can sleep too!
		}

		testCases = append(testCases, test{
			name:     "simple exec",
			res:      res,
			fail:     false,
			timeline: timeline,
			expect:   func() error { return nil },
			// build file for inotifywait
			startup: func() error { return os.WriteFile(f, []byte("starting...\n"), 0600) },
			cleanup: func() error { return os.Remove(f) },
		})
	}
	{
		r := makeRes("exec", "x2")
		res := r.(*ExecRes) // if this panics, the test will panic
		res.Env = map[string]string{
			"boiling": "one hundred",
		}
		f := filepath.Join(t.TempDir(), "whatever")
		res.Cmd = fmt.Sprintf("env | grep boiling > %q", f)
		res.Shell = "/bin/bash"
		res.IfCmd = fmt.Sprintf("! diff <(cat %q) <(echo boiling=one hundred)", f)
		res.IfShell = "/bin/bash"
		res.WatchCmd = fmt.Sprintf("/usr/bin/inotifywait -e modify -m %s", f)
		res.WatchShell = "/bin/bash"

		timeline := []Step{
			NewStartupStep(1000 * 60),              // startup
			NewChangedStep(1000*60, false),         // did we do something?
			FileExpect(f, "boiling=one hundred\n"), // check initial state
			NewClearChangedStep(1000 * 15),         // did we do something?
			FileWrite(f, "this is stuff!\n"),       // change state
			NewChangedStep(1000*60, false),         // did we do something?
			FileExpect(f, "boiling=one hundred\n"), // check again
			sleep(1),                               // we can sleep too!
		}

		testCases = append(testCases, test{
			name:     "exec with env",
			res:      res,
			fail:     false,
			timeline: timeline,
			expect:   func() error { return nil },
			// build file for inotifywait
			startup: func() error { return os.WriteFile(f, []byte("starting...\n"), 0600) },
			cleanup: func() error { return os.Remove(f) },
		})
	}
	{
		r := makeRes("file", "r1")
		res := r.(*FileRes) // if this panics, the test will panic
		p := filepath.Join(t.TempDir(), "emptyfile")
		res.Path = p
		res.State = FileStateExists

		timeline := []Step{
			NewStartupStep(1000 * 60),      // startup
			NewChangedStep(1000*60, false), // did we do something?
			FileExpect(p, ""),              // check initial state
			NewClearChangedStep(1000 * 15), // did we do something?
		}

		testCases = append(testCases, test{
			name:     "touch file",
			res:      res,
			fail:     false,
			timeline: timeline,
			expect:   func() error { return nil },
			startup:  func() error { return nil },
			cleanup:  func() error { return os.Remove(p) },
		})
	}
	{
		r := makeRes("file", "r1")
		res := r.(*FileRes) // if this panics, the test will panic
		p := filepath.Join(t.TempDir(), "existingfile")
		res.Path = p
		res.State = FileStateExists
		content := "some existing text\n"

		timeline := []Step{
			NewStartupStep(1000 * 60),     // startup
			NewChangedStep(1000*60, true), // did we do something?
			FileExpect(p, content),        // check initial state
		}

		testCases = append(testCases, test{
			name:     "existing file",
			res:      res,
			fail:     false,
			timeline: timeline,
			expect:   func() error { return nil },
			startup:  func() error { return os.WriteFile(p, []byte(content), 0600) },
			cleanup:  func() error { return os.Remove(p) },
		})
	}
	{
		r := makeRes("file", "r1")
		res := r.(*FileRes) // if this panics, the test will panic
		p := filepath.Join(t.TempDir(), "ownerfile")
		uid, err := GetUID()
		if err != nil {
			t.Fatalf("func GetUID: %v", err)
		}
		res.Path = p
		res.State = FileStateExists
		res.Owner = uid
		content := "some test file owned by uid " + uid

		timeline := []Step{
			NewStartupStep(1000 * 60),     // startup
			NewChangedStep(1000*60, true), // did we do something?
			FileExpect(p, content),        // check file content
			FileOwnerExpect(p, uid),       // check uid of the file
		}

		testCases = append(testCases, test{
			name:     "uid test file",
			res:      res,
			fail:     false,
			timeline: timeline,
			expect:   func() error { return nil },
			startup:  func() error { return os.WriteFile(p, []byte(content), 0600) },
			cleanup:  func() error { return os.Remove(p) },
		})
	}

	names := []string{}
	for index, tc := range testCases { // run all the tests
		if tc.name == "" {
			t.Errorf("test #%d: not named", index)
			continue
		}
		if util.StrInList(tc.name, names) {
			t.Errorf("test #%d: duplicate sub test name of: %s", index, tc.name)
			continue
		}
		names = append(names, tc.name)
		t.Run(fmt.Sprintf("test #%d (%s)", index, tc.name), func(t *testing.T) {
			res, fail, experr, experrstr, timeline, expect, startup, cleanup := tc.res, tc.fail, tc.experr, tc.experrstr, tc.timeline, tc.expect, tc.startup, tc.cleanup

			t.Logf("\n\ntest #%d: Res: %+v\n", index, res)
			defer t.Logf("test #%d: done!", index)

			// run validate!
			err := res.Validate()

			if !fail && err != nil {
				t.Errorf("test #%d: FAIL", index)
				t.Errorf("test #%d: could not validate Res: %+v", index, err)
				return
			}
			if fail && err == nil {
				t.Errorf("test #%d: FAIL", index)
				t.Errorf("test #%d: validate passed, expected fail", index)
				return
			}
			if fail && experr != nil && err != experr { // test for specific error!
				t.Errorf("test #%d: FAIL", index)
				t.Errorf("test #%d: expected validate fail, got wrong error", index)
				t.Errorf("test #%d: got error: %+v", index, err)
				t.Errorf("test #%d: exp error: %+v", index, experr)
				return
			}
			// test for specific error string!
			if fail && experrstr != "" && !strings.HasPrefix(err.Error(), experrstr) {
				t.Errorf("test #%d: FAIL", index)
				t.Errorf("test #%d: expected validate fail, got wrong error", index)
				t.Errorf("test #%d: got error: %s", index, err.Error())
				t.Errorf("test #%d: exp error: %s", index, experrstr)
				return
			}
			if fail && err != nil {
				t.Logf("test #%d: err: %+v", index, err)
			}

			changedChan := make(chan bool, 1) // buffered!
			readyChan := make(chan struct{})
			readyOnce := &sync.Once{}
			readyOnceFn := func() {
				close(readyChan)
			}
			eventChan := make(chan struct{})
			tmpdir := fmt.Sprintf("%s/", t.TempDir()) // gets cleaned up at end, new dir for each call
			debug := testing.Verbose()                // set via the -test.v flag to `go test`
			logf := func(format string, v ...interface{}) {
				t.Logf(fmt.Sprintf("test #%d: ", index)+format, v...)
			}
			init := &engine.Init{
				// Watch runs this to send a changed event.
				Event: func(ctx context.Context) error {
					readyOnce.Do(readyOnceFn) // only once!

					select { // this always sends one!
					case eventChan <- struct{}{}:
						return nil

					case <-ctx.Done():
						return ctx.Err()
					}
				},

				VarDir: func(p string) (string, error) {
					return path.Join(tmpdir, p), nil
				},

				// Watch listens on this for close/pause events.
				Debug: debug,
				Logf:  logf,

				// unused
				Send: func(st interface{}) error {
					return nil
				},
				Recv: func() map[string]*engine.Send {
					return map[string]*engine.Send{}
				},
			}

			if startup != nil {
				t.Logf("test #%d: running startup()", index)
				if err := startup(); err != nil {
					t.Errorf("test #%d: FAIL", index)
					t.Errorf("test #%d: could not startup: %+v", index, err)
				}
			}
			// run init
			t.Logf("test #%d: running Init", index)
			err = res.Init(init)
			defer func() {
				if cleanup == nil {
					return
				}
				t.Logf("test #%d: running cleanup()", index)
				if err := cleanup(); err != nil {
					t.Errorf("test #%d: FAIL", index)
					t.Errorf("test #%d: could not cleanup: %+v", index, err)
				}
			}()
			closeFn := func() {
				// run cleanup (we don't ever expect an error on cleanup!)
				t.Logf("test #%d: running Cleanup", index)
				if err := res.Cleanup(); err != nil {
					t.Errorf("test #%d: FAIL", index)
					t.Errorf("test #%d: could not close Res: %+v", index, err)
					//return
				}
			}

			if !fail && err != nil {
				t.Errorf("test #%d: FAIL", index)
				t.Errorf("test #%d: could not init Res: %+v", index, err)
				return
			}
			if fail && err == nil {
				closeFn() // close if Init didn't fail
				t.Errorf("test #%d: FAIL", index)
				t.Errorf("test #%d: init passed, expected fail", index)
				return
			}
			if fail && experr != nil && err != experr { // test for specific error!
				t.Errorf("test #%d: FAIL", index)
				t.Errorf("test #%d: expected init fail, got wrong error", index)
				t.Errorf("test #%d: got error: %+v", index, err)
				t.Errorf("test #%d: exp error: %+v", index, experr)
				return
			}
			// test for specific error string!
			if fail && experrstr != "" && !strings.HasPrefix(err.Error(), experrstr) {
				t.Errorf("test #%d: FAIL", index)
				t.Errorf("test #%d: expected init fail, got wrong error", index)
				t.Errorf("test #%d: got error: %s", index, err.Error())
				t.Errorf("test #%d: exp error: %s", index, experrstr)
				return
			}
			if fail && err != nil {
				t.Logf("test #%d: err: %+v", index, err)
			}
			defer closeFn()

			// run watch
			doneCtx, doneCtxCancel := context.WithCancel(context.Background())
			wg := &sync.WaitGroup{}
			defer wg.Wait() // if we return early
			defer close(changedChan)
			defer doneCtxCancel()
			wg.Add(1)
			go func() {
				defer wg.Done()
				t.Logf("test #%d: running Watch", index)
				if err := res.Watch(doneCtx); err != nil && err != context.Canceled {
					t.Errorf("test #%d: FAIL", index)
					t.Errorf("test #%d: Watch failed: %s", index, err.Error())
				}
				close(eventChan) // done with this part
			}()

			// TODO: can we block here if the test fails early?
			select {
			case <-readyChan: // called by Event() in Watch
			case _, ok := <-eventChan:
				if !ok {
					t.Errorf("test #%d: FAIL", index)
					t.Errorf("test #%d: Watch exited before startup", index)
					return
				}
			}
			wg.Add(1)
			go func() { // run timeline
				t.Logf("test #%d: executing timeline", index)
				defer wg.Done()
				for ix, step := range timeline {

					// magic setting of important values...
					if s, ok := step.(*startupStep); ok {
						s.ch = readyChan
					}
					if s, ok := step.(*changedStep); ok {
						s.ch = changedChan
					}
					if s, ok := step.(*clearChangedStep); ok {
						s.ch = changedChan
					}

					t.Logf("test #%d: step(%d)...", index, ix)
					if err := step.Action(); err != nil {
						t.Errorf("test #%d: FAIL", index)
						t.Errorf("test #%d: step(%d) action failed: %s", index, ix, err.Error())
						break
					}
					if err := step.Expect(); err != nil {
						t.Errorf("test #%d: FAIL", index)
						t.Errorf("test #%d: step(%d) expect failed: %s", index, ix, err.Error())
						break
					}
				}
				t.Logf("test #%d: shutting down Watch", index)
				doneCtxCancel() // send Watch shutdown command
			}()
		Loop:
			for {
				select {
				case _, ok := <-eventChan: // from Watch()
					if !ok {
						//t.Logf("test #%d: break!", index)
						break Loop
					}
				}

				t.Logf("test #%d: running CheckApply", index)
				checkOK, err := res.CheckApply(context.TODO(), true) // no noop!
				if err != nil {
					t.Errorf("test #%d: FAIL", index)
					t.Errorf("test #%d: CheckApply failed: %s", index, err.Error())
					return
				}
				//t.Logf("test #%d: CheckApply(true) (%t, %+v)", index, checkOK, err)
				select {
				// send a msg if we can, but never block
				case changedChan <- checkOK:
				default:
				}
			}

			t.Logf("test #%d: waiting for shutdown", index)
			wg.Wait()

			if err := expect(); err != nil {
				t.Errorf("test #%d: FAIL", index)
				t.Errorf("test #%d: expect failed: %s", index, err.Error())
				return
			}

			// all done!
		})
	}
}

// TestResources2 just tests a partial execution of the resource by running
// CheckApply and Reverse and basics without the mainloop. It's a less accurate
// representation of a running resource, but is still useful for many
// circumstances. This also uses a simpler timeline, because it was not possible
// to get the reference passing of the reversed resource working with the fancy
// version.
func TestResources2(t *testing.T) {
	type test struct { // an individual test
		name     string
		timeline []func() error // TODO: this could be a generator that keeps pushing out steps until it's done!
		expect   func() error   // function to check for expected state
		startup  func() error   // function to run as startup (unused?)
		cleanup  func() error   // function to run as cleanup
	}

	type initOptions struct {
		// graph is the graph that should be passed in with Init
		graph *pgraph.Graph
		// TODO: add more options if needed

		// logf specifies the log function for Init to pass through...
		logf func(format string, v ...interface{})
	}

	type initOption func(*initOptions)

	addGraph := func(graph *pgraph.Graph) initOption {
		return func(io *initOptions) {
			io.graph = graph
		}
	}

	addLogf := func(logf func(format string, v ...interface{})) initOption {
		return func(io *initOptions) {
			io.logf = logf
		}
	}

	// resValidate runs Validate on the res.
	resValidate := func(res engine.Res) func() error {
		// run Close
		return func() error {
			return res.Validate()
		}
	}
	// resInit runs Init on the res.
	resInit := func(res engine.Res, opts ...initOption) func() error {

		io := &initOptions{}              // defaults
		for _, optionFunc := range opts { // apply the options
			optionFunc(io)
		}

		logf := func(format string, v ...interface{}) {
			if io.logf == nil {
				return
			}
			io.logf(fmt.Sprintf("test: ")+format+"\n", v...)
		}
		init := &engine.Init{
			//Debug: debug,
			Logf: logf,

			// unused
			Event: func(ctx context.Context) error {
				return nil
			},

			// unused
			Send: func(st interface{}) error {
				return nil
			},
			Recv: func() map[string]*engine.Send {
				return map[string]*engine.Send{}
			},

			// Copied from state.go
			FilteredGraph: func() (*pgraph.Graph, error) {
				//graph, err := pgraph.NewGraph("filtered")
				//if err != nil {
				//	return nil, err
				//}
				// Hack: We just add ourself as allowed since
				// we're just a one-vertex test suite...
				//graph.AddVertex(res) // hack!
				//return graph, nil // we return in a func so it's fresh!
				if io.graph == nil {
					return nil, fmt.Errorf("use addGraph to add one here")
				}
				return io.graph, nil
			},
		}
		// run Init
		return func() error {
			return res.Init(init)

		}
	}
	// resCheckApplyError runs CheckApply with noop = false for the res. It
	// errors if the returned checkOK values isn't what we were expecting or
	// if the errOK function returns an error when given a chance to inspect
	// the returned error.
	resCheckApplyError := func(res engine.Res, expCheckOK bool, errOK func(e error) error) func() error {
		return func() error {
			checkOK, err := res.CheckApply(context.TODO(), true) // no noop!
			if e := errOK(err); e != nil {
				return errwrap.Wrapf(e, "error from CheckApply did not match expected")
			}
			if checkOK != expCheckOK {
				return fmt.Errorf("result from CheckApply did not match expected: got: %t exp: %t", checkOK, expCheckOK)
			}
			return nil
		}
	}
	// resCheckApply runs CheckApply with noop = false for the res. It
	// errors if the returned checkOK values isn't what we were expecting or
	// if there was an error.
	resCheckApply := func(res engine.Res, expCheckOK bool) func() error {
		errOK := func(e error) error {
			if e == nil {
				return nil
			}
			return errwrap.Wrapf(e, "unexpected error from CheckApply")
		}
		return resCheckApplyError(res, expCheckOK, errOK)
	}
	// resCleanup runs Cleanup on the res.
	resCleanup := func(res engine.Res) func() error {
		// run Cleanup
		return func() error {
			return res.Cleanup()
		}
	}
	// resReversal runs Reverse on the resource and stores the result in the
	// rev variable. This should be called before the res CheckApply, and
	// usually before Init, but after Validate.
	resReversal := func(res engine.Res, rev *engine.Res) func() error {
		return func() error {
			r, ok := res.(engine.ReversibleRes)
			if !ok {
				return fmt.Errorf("res is not a ReversibleRes")
			}

			// We don't really need this to be checked here.
			//if r.ReversibleMeta().Disabled {
			//	return fmt.Errorf("res did not specify Meta:reverse")
			//}

			if r.ReversibleMeta().Reversal {
				//logf("triangle reversal") // warn!
			}

			reversed, err := r.Reversed()
			if err != nil {
				return errwrap.Wrapf(err, "could not reverse: %s", r.String())
			}
			if reversed == nil {
				return nil // this can't be reversed, or isn't implemented here
			}

			reversed.ReversibleMeta().Reversal = true // set this for later...

			retRes, ok := reversed.(engine.Res)
			if !ok {
				return fmt.Errorf("not a Res")
			}

			*rev = retRes // store!
			return nil
		}
	}
	fileWrite := func(p, s string) func() error {
		// write the file to path
		return func() error {
			return os.WriteFile(p, []byte(s), 0600)
		}
	}
	fileExpect := func(p, s string) func() error {
		// check the contents at the path match the string we expect
		return func() error {
			content, err := os.ReadFile(p)
			if err != nil {
				return err
			}
			if string(content) != s {
				return fmt.Errorf("contents did not match in %s", p)
			}
			return nil
		}
	}
	fileExists := func(p string, dir bool) func() error {
		// does the file exist?
		return func() error {
			fi, err := os.Stat(p)
			if err != nil {
				return fmt.Errorf("file was supposed to be present, got: %+v", err)
			}
			if fi.IsDir() != dir {
				if dir {
					return fmt.Errorf("not a dir")
				}
				return fmt.Errorf("not a regular file")
			}
			return nil
		}
	}
	fileAbsent := func(p string) func() error {
		// does the file exist?
		return func() error {
			_, err := os.Stat(p)
			if err == nil {
				return fmt.Errorf("file exists, expecting absent")
			}
			if !os.IsNotExist(err) {
				return fmt.Errorf("file was supposed to be absent, got: %+v", err)
			}
			return nil
		}
	}
	fileRemove := func(p string) func() error {
		// remove the file at path
		return func() error {
			err := os.Remove(p)
			// if the file isn't there, don't error
			if err != nil && !os.IsNotExist(err) {
				return err
			}
			return nil
		}
	}
	fileMkdir := func(p string, all bool) func() error {
		// mkdir at the path
		return func() error {
			if all {
				return os.MkdirAll(p, 0750)
			}
			return os.Mkdir(p, 0750)
		}
	}

	tmpdir := t.TempDir()
	testCases := []test{}
	{
		//file "/tmp/somefile" {
		//	state => $const.res.file.state.exists,
		//	content => "some new text\n",
		//}
		r1 := makeRes("file", "r1")
		res := r1.(*FileRes) // if this panics, the test will panic
		p := filepath.Join(tmpdir, "somefile")
		res.Path = p
		res.State = FileStateExists
		content := "some new text\n"
		res.Content = &content

		timeline := []func() error{
			fileWrite(p, "whatever"),
			resValidate(r1),
			resInit(r1),
			resCheckApply(r1, false), // changed
			fileExpect(p, content),
			resCheckApply(r1, true), // it's already good
			resCleanup(r1),
			fileExpect(p, content), // ensure it exists
		}

		testCases = append(testCases, test{
			name:     "simple file",
			timeline: timeline,
			expect:   func() error { return nil },
			startup:  func() error { return nil },
			cleanup:  func() error { return nil },
		})
	}
	{
		//file "/tmp/somefile" {
		//	# state is NOT specified
		//	content => "some new text\n",
		//}
		r1 := makeRes("file", "r1")
		res := r1.(*FileRes) // if this panics, the test will panic
		p := filepath.Join(tmpdir, "somefile")
		res.Path = p
		//res.State = FileStateExists // not specified!
		content := "some new text\n"
		res.Content = &content

		timeline := []func() error{
			fileWrite(p, "whatever"),
			resValidate(r1),
			resInit(r1),
			resCheckApply(r1, false), // changed
			fileExpect(p, content),
			resCheckApply(r1, true), // it's already good
			resCleanup(r1),
			fileExpect(p, content), // ensure it exists
		}

		testCases = append(testCases, test{
			name:     "edit file only",
			timeline: timeline,
			expect:   func() error { return nil },
			startup:  func() error { return nil },
			cleanup:  func() error { return nil },
		})
	}
	{
		//file "/tmp/somefile" {
		//	# state is NOT specified
		//	content => "some new text\n",
		//}
		// and no existing file exists! (therefore we want an error!)
		r1 := makeRes("file", "r1")
		res := r1.(*FileRes) // if this panics, the test will panic
		p := filepath.Join(tmpdir, "somefile")
		res.Path = p
		//res.State = FileStateExists // not specified!
		content := "some new text\n"
		res.Content = &content

		timeline := []func() error{
			fileRemove(p), // nothing here
			resValidate(r1),
			resInit(r1),
			resCheckApplyError(r1, false, ErrIsNotExistOK), // should error
			resCheckApplyError(r1, false, ErrIsNotExistOK), // double check
			resCleanup(r1),
			fileAbsent(p), // ensure it's absent
		}

		testCases = append(testCases, test{
			name:     "strict file",
			timeline: timeline,
			expect:   func() error { return nil },
			startup:  func() error { return nil },
			cleanup:  func() error { return nil },
		})
	}
	{
		//file "/tmp/somefile" {
		//	state => $const.res.file.state.absent,
		//}
		// and no existing file exists!
		r1 := makeRes("file", "r1")
		res := r1.(*FileRes) // if this panics, the test will panic
		p := filepath.Join(tmpdir, "somefile")
		res.Path = p
		res.State = FileStateAbsent

		timeline := []func() error{
			fileRemove(p), // nothing here
			resValidate(r1),
			resInit(r1),
			resCheckApply(r1, true),
			resCheckApply(r1, true),
			resCleanup(r1),
			fileAbsent(p), // ensure it's absent
		}

		testCases = append(testCases, test{
			name:     "absent file",
			timeline: timeline,
			expect:   func() error { return nil },
			startup:  func() error { return nil },
			cleanup:  func() error { return nil },
		})
	}
	{
		//file "/tmp/somefile" {
		//	state => $const.res.file.state.absent,
		//}
		// and a file already exists!
		r1 := makeRes("file", "r1")
		res := r1.(*FileRes) // if this panics, the test will panic
		p := filepath.Join(tmpdir, "somefile")
		res.Path = p
		res.State = FileStateAbsent

		timeline := []func() error{
			fileWrite(p, "whatever"),
			resValidate(r1),
			resInit(r1),
			resCheckApply(r1, false),
			resCheckApply(r1, true),
			resCleanup(r1),
			fileAbsent(p), // ensure it's absent
		}

		testCases = append(testCases, test{
			name:     "absent file pre-existing",
			timeline: timeline,
			expect:   func() error { return nil },
			startup:  func() error { return nil },
			cleanup:  func() error { return nil },
		})
	}
	{
		//file "/tmp/somefile" {
		//	content => "some new text\n",
		//	state => $const.res.file.state.exists,
		//
		//	Meta:reverse => true,
		//}
		r1 := makeRes("file", "r1")
		res := r1.(*FileRes) // if this panics, the test will panic
		p := filepath.Join(tmpdir, "somefile")
		res.Path = p
		res.State = FileStateExists
		content := "some new text\n"
		res.Content = &content
		original := "this is the original state\n" // original state
		var r2 engine.Res                          // future reversed resource

		timeline := []func() error{
			fileWrite(p, original),
			fileExpect(p, original),
			resValidate(r1),
			resReversal(r1, &r2), // runs in Init to snapshot
			func() error { // random test
				if st := r2.(*FileRes).State; st != "absent" {
					return fmt.Errorf("unexpected state: %s", st)
				}
				return nil
			},
			resInit(r1),
			resCheckApply(r1, false), // changed
			fileExpect(p, content),
			resCheckApply(r1, true), // it's already good
			resCleanup(r1),
			//resValidate(r2), // no!!!
			func() error {
				// wrap it b/c it is currently nil
				return r2.Validate()
			},
			func() error {
				return resInit(r2)()
			},
			func() error {
				return resCheckApply(r2, false)()
			},
			func() error {
				return resCheckApply(r2, true)()
			},
			func() error {
				return resCleanup(r2)()
			},
			fileAbsent(p), // ensure it's absent
		}

		testCases = append(testCases, test{
			name:     "some file",
			timeline: timeline,
			expect:   func() error { return nil },
			startup:  func() error { return nil },
			cleanup:  func() error { return nil },
		})
	}
	{
		//file "/tmp/somefile" {
		//	content => "some new text\n",
		//
		//	Meta:reverse => true,
		//}
		//# and there's an existing file at this path...
		r1 := makeRes("file", "r1")
		res := r1.(*FileRes) // if this panics, the test will panic
		p := filepath.Join(tmpdir, "somefile")
		res.Path = p
		//res.State = FileStateExists // unspecified
		content := "some new text\n"
		res.Content = &content
		original := "this is the original state\n" // original state
		var r2 engine.Res                          // future reversed resource

		timeline := []func() error{
			fileWrite(p, original),
			fileExpect(p, original),
			resValidate(r1),
			resReversal(r1, &r2), // runs in Init to snapshot
			func() error { // random test
				// state should be unspecified
				if st := r2.(*FileRes).State; st == "absent" || st == "exists" {
					return fmt.Errorf("unexpected state: %s", st)
				}
				return nil
			},
			resInit(r1),
			resCheckApply(r1, false), // changed
			fileExpect(p, content),
			resCheckApply(r1, true), // it's already good
			resCleanup(r1),
			//resValidate(r2),
			func() error {
				// wrap it b/c it is currently nil
				return r2.Validate()
			},
			func() error {
				return resInit(r2)()
			},
			func() error {
				return resCheckApply(r2, false)()
			},
			func() error {
				return resCheckApply(r2, true)()
			},
			func() error {
				return resCleanup(r2)()
			},
			fileExpect(p, original), // we restored the contents!
			fileRemove(p),           // cleanup
		}

		testCases = append(testCases, test{
			name:     "some file restore",
			timeline: timeline,
			expect:   func() error { return nil },
			startup:  func() error { return nil },
			cleanup:  func() error { return nil },
		})
	}
	{
		//file "/tmp/somefile" {
		//	content => "some new text\n",
		//
		//	Meta:reverse => true,
		//}
		//# and there's NO existing file at this path...
		//# NOTE: This used to be a corner case subtlety for reversal.
		//# Now that we error in this scenario before reversal, it's ok!
		r1 := makeRes("file", "r1")
		res := r1.(*FileRes) // if this panics, the test will panic
		p := filepath.Join(tmpdir, "somefile")
		res.Path = p
		//res.State = FileStateExists // unspecified
		content := "some new text\n"
		res.Content = &content
		var r2 engine.Res // future reversed resource

		timeline := []func() error{
			fileRemove(p), // ensure no file exists
			resValidate(r1),
			resReversal(r1, &r2), // runs in Init to snapshot
			func() error { // random test
				// state should be unspecified i think
				// TODO: or should it be absent?
				if st := r2.(*FileRes).State; st == "absent" || st == "exists" {
					return fmt.Errorf("unexpected state: %s", st)
				}
				return nil
			},
			resInit(r1),
			resCheckApplyError(r1, false, ErrIsNotExistOK), // changed
			//fileExpect(p, content),
			//resCheckApply(r1, true), // it's already good
			resCleanup(r1),
			//func() error {
			//	// wrap it b/c it is currently nil
			//	return r2.Validate()
			//},
			//func() error {
			//	return resInit(r2)()
			//},
			//func() error { // it's already in the correct state
			//	return resCheckApply(r2, true)()
			//},
			//func() error {
			//	return resCleanup(r2)()
			//},
			//fileExpect(p, content), // we never changed it back...
			//fileRemove(p),          // cleanup
		}

		testCases = append(testCases, test{
			name:     "ambiguous file restore",
			timeline: timeline,
			expect:   func() error { return nil },
			startup:  func() error { return nil },
			cleanup:  func() error { return nil },
		})
	}
	{
		//file "/tmp/somefile" {
		//	state => $const.res.file.state.absent,
		//
		//	Meta:reverse => true,
		//}
		r1 := makeRes("file", "r1")
		res := r1.(*FileRes) // if this panics, the test will panic
		p := filepath.Join(tmpdir, "somefile")
		res.Path = p
		res.State = FileStateAbsent
		original := "this is the original state\n" // original state
		var r2 engine.Res                          // future reversed resource

		timeline := []func() error{
			fileWrite(p, original),
			fileExpect(p, original),
			resValidate(r1),
			resReversal(r1, &r2), // runs in Init to snapshot
			func() error { // random test
				if st := r2.(*FileRes).State; st != "exists" {
					return fmt.Errorf("unexpected state: %s", st)
				}
				return nil
			},
			resInit(r1),
			resCheckApply(r1, false), // changed
			fileAbsent(p),            // ensure it got removed
			resCheckApply(r1, true),  // it's already good
			resCleanup(r1),
			//resValidate(r2), // no!!!
			func() error {
				// wrap it b/c it is currently nil
				return r2.Validate()
			},
			func() error {
				return resInit(r2)()
			},
			func() error {
				return resCheckApply(r2, false)()
			},
			func() error {
				return resCheckApply(r2, true)()
			},
			func() error {
				return resCleanup(r2)()
			},
			fileExpect(p, original), // ensure it's back to original
		}

		testCases = append(testCases, test{
			name:     "some removal",
			timeline: timeline,
			expect:   func() error { return nil },
			startup:  func() error { return nil },
			cleanup:  func() error { return nil },
		})
	}
	{
		//file "/tmp/somefile" {
		//	state => $const.res.file.state.exists,
		//	fragments => [
		//		"/tmp/frag1",
		//		"/tmp/fragdir1/",
		//		"/tmp/frag2",
		//		"/tmp/fragdir2/",
		//		"/tmp/frag3",
		//	],
		//}
		r1 := makeRes("file", "r1")
		res := r1.(*FileRes) // if this panics, the test will panic
		p := filepath.Join(tmpdir, "somefile")
		res.Path = p
		res.State = FileStateExists
		frag1Path := filepath.Join(tmpdir, "frag1")
		fragDir1 := filepath.Join(tmpdir, "fragdir1")
		frag2Path := filepath.Join(tmpdir, "frag2")
		fragDir2 := filepath.Join(tmpdir, "fragdir2")
		frag3Path := filepath.Join(tmpdir, "frag3")
		res.Fragments = []string{
			frag1Path,
			fragDir1 + "/",
			frag2Path,
			fragDir2 + "/",
			frag3Path,
		}

		frag1 := "frag1\n"
		f1 := "f1\n"
		f2 := "f2\n"
		f3 := "f3\n"
		frag2 := "frag2\n"
		f1d2 := "f1 from fragdir2\n"
		f2d2 := "f2 from fragdir2\n"
		f3d2 := "f3 from fragdir2\n"
		frag3 := "frag3\n"
		content := frag1 + f1 + f2 + f3 + frag2 + f1d2 + f2d2 + f3d2 + frag3

		timeline := []func() error{
			fileWrite(frag1Path, frag1),
			fileWrite(frag2Path, frag2),
			fileWrite(frag3Path, frag3),
			fileMkdir(fragDir1, true),
			fileWrite(filepath.Join(fragDir1, "f1"), f1),
			fileWrite(filepath.Join(fragDir1, "f2"), f2),
			fileWrite(filepath.Join(fragDir1, "f3"), f3),
			fileMkdir(fragDir2, true),
			fileWrite(filepath.Join(fragDir2, "f1"), f1d2),
			fileWrite(filepath.Join(fragDir2, "f2"), f2d2),
			fileWrite(filepath.Join(fragDir2, "f3"), f3d2),
			fileWrite(p, "whatever"),
			resValidate(r1),
			resInit(r1),
			resCheckApply(r1, false), // changed
			fileExpect(p, content),
			resCheckApply(r1, true), // it's already good
			resCleanup(r1),
			fileExpect(p, content), // ensure it exists
		}

		testCases = append(testCases, test{
			name:     "file fragments",
			timeline: timeline,
			expect:   func() error { return nil },
			startup:  func() error { return nil },
			cleanup:  func() error { return os.Remove(p) },
		})
	}
	{
		//file "/tmp/somefile" {
		//	state => $const.res.file.state.exists,
		//	source => "/tmp/somefiletocopy",
		//}
		r1 := makeRes("file", "r1")
		res := r1.(*FileRes) // if this panics, the test will panic
		p := filepath.Join(tmpdir, "somefile")
		p2 := filepath.Join(tmpdir, "somefiletocopy")
		content := "hello this is some file to copy\n"
		res.Path = p
		res.State = FileStateExists
		res.Source = p2

		timeline := []func() error{
			fileAbsent(p), // ensure it's absent
			fileWrite(p2, content),
			resValidate(r1),
			resInit(r1),
			resCheckApply(r1, false), // changed
			fileExpect(p, content),   // should be created like this
			fileExpect(p2, content),  // should not change
			resCheckApply(r1, true),  // it's already good
			fileExpect(p, content),   // should already be like this
			fileExpect(p2, content),  // should not change either
			resCleanup(r1),
		}

		testCases = append(testCases, test{
			name:     "copy file with source",
			timeline: timeline,
			expect:   func() error { return nil },
			startup:  func() error { return nil },
			cleanup:  func() error { return os.Remove(p) },
		})
	}
	{
		//file "/tmp/somedir/" {
		//	state => $const.res.file.state.exists,
		//}
		r1 := makeRes("file", "r1")
		res := r1.(*FileRes) // if this panics, the test will panic
		p := filepath.Join(tmpdir, "somedir") + "/"
		res.Path = p
		res.State = FileStateExists

		timeline := []func() error{
			fileAbsent(p), // ensure it's absent
			resValidate(r1),
			resInit(r1),
			resCheckApply(r1, false), // changed
			fileExists(p, true),      // ensure it's a dir
			resCheckApply(r1, true),  // it's already good
			fileExists(p, true),      // ensure it's a dir
			resCleanup(r1),
		}

		testCases = append(testCases, test{
			name:     "make empty directory",
			timeline: timeline,
			expect:   func() error { return nil },
			startup:  func() error { return nil },
			cleanup:  func() error { return os.RemoveAll(p) },
		})
	}
	{
		//file "/tmp/somedir/" {
		//	state => $const.res.file.state.exists,
		//	source => /tmp/somedirtocopy/,
		//	recurse => true,
		//}
		r1 := makeRes("file", "r1")
		res := r1.(*FileRes) // if this panics, the test will panic
		p := filepath.Join(tmpdir, "somedir") + "/"
		p2 := filepath.Join(tmpdir, "somedirtocopy") + "/"
		res.Path = p
		res.State = FileStateExists
		res.Source = p2
		res.Recurse = true

		f1 := path.Join(p, "f1")
		f2 := path.Join(p, "f2")
		d1 := path.Join(p, "d1/")
		d2 := path.Join(p, "d2/")
		d1f1 := path.Join(p, "d1/f1")
		d1f2 := path.Join(p, "d1/f2")
		d2f1 := path.Join(p, "d2/f1")
		d2f2 := path.Join(p, "d2/f2")
		d2f3 := path.Join(p, "d2/f3")

		xf1 := path.Join(p2, "f1")
		xf2 := path.Join(p2, "f2")
		xd1 := path.Join(p2, "d1/")
		xd2 := path.Join(p2, "d2/")
		xd1f1 := path.Join(p2, "d1/f1")
		xd1f2 := path.Join(p2, "d1/f2")
		xd2f1 := path.Join(p2, "d2/f1")
		xd2f2 := path.Join(p2, "d2/f2")
		xd2f3 := path.Join(p2, "d2/f3")

		timeline := []func() error{
			fileMkdir(p2, true),
			fileWrite(xf1, "f1\n"),
			fileWrite(xf2, "f2\n"),
			fileMkdir(xd1, true),
			fileMkdir(xd2, true),
			fileWrite(xd1f1, "d1f1\n"),
			fileWrite(xd1f2, "d1f2\n"),
			fileWrite(xd2f1, "d2f1\n"),
			fileWrite(xd2f2, "d2f2\n"),
			fileWrite(xd2f3, "d2f3\n"),
			resValidate(r1),
			resInit(r1),
			resCheckApply(r1, false), // changed
			fileExists(p, true),      // ensure it's a dir
			fileExists(f1, false),    // ensure it's a file
			fileExists(f2, false),
			fileExists(d1, true), // ensure it's a dir
			fileExists(d2, true),
			fileExists(d1f1, false),
			fileExists(d1f2, false),
			fileExists(d2f1, false),
			fileExists(d2f2, false),
			fileExists(d2f3, false),
			resCheckApply(r1, true), // it's already good
			resCleanup(r1),
		}

		testCases = append(testCases, test{
			name:     "source dir copy",
			timeline: timeline,
			expect:   func() error { return nil },
			startup:  func() error { return nil },
			cleanup:  func() error { return os.RemoveAll(p) },
		})
	}
	{
		//file "/tmp/somedir/" {
		//	state => $const.res.file.state.exists,
		//	recurse => true,
		//	purge => true,
		//}
		r1 := makeRes("file", "r1")
		res := r1.(*FileRes) // if this panics, the test will panic
		p := filepath.Join(tmpdir, "somedir") + "/"
		res.Path = p
		res.State = FileStateExists
		res.Recurse = true
		res.Purge = true

		f1 := path.Join(p, "f1")
		f2 := path.Join(p, "f2")
		d1 := path.Join(p, "d1/")
		d2 := path.Join(p, "d2/")
		d1f1 := path.Join(p, "d1/f1")
		d1f2 := path.Join(p, "d1/f2")
		d2f1 := path.Join(p, "d2/f1")
		d2f2 := path.Join(p, "d2/f2")
		d2f3 := path.Join(p, "d2/f3")

		graph, err := pgraph.NewGraph("test")
		if err != nil {
			panic("can't make graph")
		}
		graph.AddVertex(res) // add self

		timeline := []func() error{
			fileMkdir(p, true),
			fileWrite(f1, "f1\n"),
			fileWrite(f2, "f2\n"),
			fileMkdir(d1, true),
			fileMkdir(d2, true),
			fileWrite(d1f1, "d1f1\n"),
			fileWrite(d1f2, "d1f2\n"),
			fileWrite(d2f1, "d2f1\n"),
			fileWrite(d2f2, "d2f2\n"),
			fileWrite(d2f3, "d2f3\n"),
			resValidate(r1),
			resInit(r1, addGraph(graph)),
			resCheckApply(r1, false), // changed
			fileExists(p, true),      // ensure it's a dir
			fileAbsent(f1),           // ensure it's absent
			fileAbsent(f2),
			fileAbsent(d1),
			fileAbsent(d2),
			fileAbsent(d1f1),
			fileAbsent(d1f2),
			fileAbsent(d2f1),
			fileAbsent(d2f2),
			fileAbsent(d2f3),
			resCheckApply(r1, true), // it's already good
			resCleanup(r1),
		}

		testCases = append(testCases, test{
			name:     "dir purge",
			timeline: timeline,
			expect:   func() error { return nil },
			startup:  func() error { return nil },
			cleanup:  func() error { return os.RemoveAll(p) },
		})
	}
	{
		//file "/tmp/somedir/" {
		//	state => $const.res.file.state.exists,
		//	recurse => true,
		//	purge => true,
		//}
		// TODO: should State be required for these to not delete them?
		//file "/tmp/somedir/hello" {
		//}
		//file "/tmp/somedir/nested-dir/" {
		//}
		//file "/tmp/somedir/nested-dir/nestedfileindir" {
		//}
		r1 := makeRes("file", "r1")
		res := r1.(*FileRes) // if this panics, the test will panic
		p := filepath.Join(tmpdir, "somedir") + "/"
		res.Path = p
		res.State = FileStateExists
		res.Recurse = true
		res.Purge = true

		f1 := path.Join(p, "f1")
		f2 := path.Join(p, "f2")
		d1 := path.Join(p, "d1/")
		d2 := path.Join(p, "d2/")
		d1f1 := path.Join(p, "d1/f1")
		d1f2 := path.Join(p, "d1/f2")
		d2f1 := path.Join(p, "d2/f1")
		d2f2 := path.Join(p, "d2/f2")
		d2f3 := path.Join(p, "d2/f3")

		r2 := makeRes("file", "r2")
		res2 := r2.(*FileRes)
		p2 := path.Join(p, "hello")
		res2.Path = p2
		p2c := "i am a hello file\n"
		// TODO: should State be required for this to not delete it?

		r3 := makeRes("file", "r3")
		res3 := r3.(*FileRes)
		p3 := path.Join(p, "nested-dir/")
		res3.Path = p3
		// TODO: should State be required for this to not delete it?

		r4 := makeRes("file", "r4")
		res4 := r4.(*FileRes)
		p4 := path.Join(p3, "nestedfileindir")
		res4.Path = p4
		p4c := "i am a nested file\n"
		// TODO: should State be required for this to not delete it?

		graph, err := pgraph.NewGraph("test")
		if err != nil {
			panic("can't make graph")
		}
		graph.AddVertex(res, res2, res3, res4)

		timeline := []func() error{
			fileMkdir(p, true),
			fileWrite(f1, "f1\n"),
			fileWrite(f2, "f2\n"),
			fileMkdir(d1, true),
			fileMkdir(d2, true),
			fileWrite(d1f1, "d1f1\n"),
			fileWrite(d1f2, "d1f2\n"),
			fileWrite(d2f1, "d2f1\n"),
			fileWrite(d2f2, "d2f2\n"),
			fileWrite(d2f3, "d2f3\n"),
			fileWrite(p2, p2c),
			fileMkdir(p3, true),
			fileWrite(p4, p4c),

			resValidate(r2),
			resInit(r2),
			//resCheckApply(r2, false), // not really needed in test
			resCleanup(r2),

			resValidate(r3),
			resInit(r3),
			//resCheckApply(r3, false), // not really needed in test
			resCleanup(r3),

			resValidate(r4),
			resInit(r4),
			//resCheckApply(r4, false), // not really needed in test
			resCleanup(r4),

			resValidate(r1),
			resInit(r1, addGraph(graph), addLogf(nil)), // show the full graph
			resCheckApply(r1, false),                   // changed
			fileExists(p, true),                        // ensure it's a dir
			fileAbsent(f1),                             // ensure it's absent
			fileAbsent(f2),
			fileAbsent(d1),
			fileAbsent(d2),
			fileAbsent(d1f1),
			fileAbsent(d1f2),
			fileAbsent(d2f1),
			fileAbsent(d2f2),
			fileAbsent(d2f3),
			fileExists(p2, false), // ensure it's a file
			fileExists(p3, true),  // ensure it's a dir
			fileExists(p4, false),
			resCheckApply(r1, true), // it's already good
			resCleanup(r1),
		}

		testCases = append(testCases, test{
			name:     "dir purge with others inside",
			timeline: timeline,
			expect:   func() error { return nil },
			startup:  func() error { return nil },
			cleanup:  func() error { return os.RemoveAll(p) },
		})
	}
	names := []string{}
	for index, tc := range testCases { // run all the tests
		if tc.name == "" {
			t.Errorf("test #%d: not named", index)
			continue
		}
		if util.StrInList(tc.name, names) {
			t.Errorf("test #%d: duplicate sub test name of: %s", index, tc.name)
			continue
		}
		names = append(names, tc.name)
		t.Run(fmt.Sprintf("test #%d (%s)", index, tc.name), func(t *testing.T) {
			timeline, expect, startup, cleanup := tc.timeline, tc.expect, tc.startup, tc.cleanup

			t.Logf("test #%d: starting...\n", index)
			defer t.Logf("test #%d: done!", index)

			//debug := testing.Verbose() // set via the -test.v flag to `go test`
			//logf := func(format string, v ...interface{}) {
			//	t.Logf(fmt.Sprintf("test #%d: ", index)+format, v...)
			//}

			if startup != nil {
				t.Logf("test #%d: running startup()", index)
				if err := startup(); err != nil {
					t.Errorf("test #%d: FAIL", index)
					t.Errorf("test #%d: could not startup: %+v", index, err)
				}
			}
			defer func() {
				if cleanup == nil {
					return
				}
				t.Logf("test #%d: running cleanup()", index)
				if err := cleanup(); err != nil {
					t.Errorf("test #%d: FAIL", index)
					t.Errorf("test #%d: could not cleanup: %+v", index, err)
				}
			}()

			// run timeline
			t.Logf("test #%d: executing timeline", index)
			for ix, step := range timeline {
				t.Logf("test #%d: step(%d)...", index, ix)
				if err := step(); err != nil {
					t.Errorf("test #%d: FAIL", index)
					t.Errorf("test #%d: step(%d) action failed: %s", index, ix, err.Error())
					break
				}
			}

			t.Logf("test #%d: shutting down...", index)

			if err := expect(); err != nil {
				t.Errorf("test #%d: FAIL", index)
				t.Errorf("test #%d: expect failed: %s", index, err.Error())
				return
			}

			// all done!
		})
	}
}

func TestResPtrUID1(t *testing.T) {
	t1, err := engine.NewNamedResource("test", "test1")
	if err != nil {
		t.Errorf("could not build resource: %+v", err)
		return
	}
	t2, err := engine.NewNamedResource("test", "test1")
	if err != nil {
		t.Errorf("could not build resource: %+v", err)
		return
	}

	if uid1, uid2 := engine.PtrUID(t1), engine.PtrUID(t2); uid1 != uid2 {
		t.Errorf("uid's don't match")
	}
}

func TestResToB64(t *testing.T) {
	res, err := engine.NewNamedResource("noop", "n1")
	if err != nil {
		t.Errorf("could not build resource: %+v", err)
		return
	}

	s, err := engineUtil.ResToB64(res)
	if err != nil {
		t.Errorf("error trying to encode res: %s", err.Error())
		return
	}
	t.Logf("out: %s", s)
}

func TestResToB64Meta(t *testing.T) {
	hidden := true // must be true, since false is a default

	res, err := engine.NewNamedResource("noop", "n1")
	if err != nil {
		t.Errorf("could not build resource: %+v", err)
		return
	}
	res.MetaParams().Hidden = hidden

	s, err := engineUtil.ResToB64(res)
	if err != nil {
		t.Errorf("error trying to encode res: %s", err.Error())
		return
	}
	t.Logf("out: %s", s)

	r, err := engineUtil.B64ToRes(s)
	if err != nil {
		t.Errorf("error trying to decode res: %s", err.Error())
		return
	}
	if r.MetaParams().Hidden != hidden {
		t.Errorf("metaparam did not get preserved")
		return
	}
	t.Logf("meta: %v", r.MetaParams().Hidden)
}
