github.com/jmigpin/editor@v1.6.0/util/testutil/script.go (about)

     1  package testutil
     2  
     3  import (
     4  	"bufio"
     5  	"bytes"
     6  	"context"
     7  	"errors"
     8  	"fmt"
     9  	"os"
    10  	"os/exec"
    11  	"path/filepath"
    12  	"strconv"
    13  	"strings"
    14  	"testing"
    15  
    16  	"github.com/jmigpin/editor/util/iout"
    17  	"github.com/jmigpin/editor/util/osutil"
    18  	"golang.org/x/tools/txtar"
    19  )
    20  
    21  // based on txtar (txt archive)
    22  type Script struct {
    23  	ScriptsDir string
    24  	Args       []string
    25  	Cmds       []*ScriptCmd // user cmds (provided)
    26  	Work       bool         // don't remove work dir at end
    27  
    28  	ScriptStart func(*testing.T) error // each script init
    29  	ScriptStop  func(*testing.T) error // each script close
    30  
    31  	ucmds map[string]*ScriptCmd // user cmds (mapped)
    32  	icmds map[string]*ScriptCmd // internal cmds
    33  
    34  	workDir    string
    35  	lastCmdStd [2][]byte // stdin, stdout
    36  	lastCmd    struct {
    37  		stdout []byte
    38  		stderr []byte
    39  		err    []byte
    40  	}
    41  }
    42  
    43  func NewScript(args []string) *Script {
    44  	return &Script{Args: args}
    45  }
    46  
    47  //----------
    48  
    49  func (scr *Script) log(t *testing.T, s string) {
    50  	s = strings.TrimRight(s, "\n") // remove newlines
    51  	t.Log(s)                       // adds one newline
    52  }
    53  func (scr *Script) logf(t *testing.T, f string, args ...interface{}) {
    54  	scr.log(t, fmt.Sprintf(f, args...))
    55  }
    56  
    57  //----------
    58  
    59  func (scr *Script) Run(t *testing.T) {
    60  	// internal cmds
    61  	icmds := []*ScriptCmd{
    62  		&ScriptCmd{"ucmd", scr.icUCmd}, // run user cmd
    63  		&ScriptCmd{"exec", scr.icExec},
    64  		&ScriptCmd{"contains", scr.icContains},
    65  		&ScriptCmd{"setenv", scr.icSetEnv},
    66  		&ScriptCmd{"fail", scr.icFail},
    67  		&ScriptCmd{"cd", scr.icChangeDir},
    68  	}
    69  	scr.icmds = mapScriptCmds(icmds)
    70  	// user cmds
    71  	scr.ucmds = mapScriptCmds(scr.Cmds)
    72  
    73  	if err := scr.runDir(t, scr.ScriptsDir); err != nil {
    74  		t.Fatal(err)
    75  	}
    76  }
    77  func (scr *Script) runDir(t *testing.T, dir string) error {
    78  	des, err := os.ReadDir(dir)
    79  	if err != nil {
    80  		return err
    81  	}
    82  	for _, de := range des {
    83  		if de.IsDir() {
    84  			continue
    85  		}
    86  		filename := filepath.Join(dir, de.Name())
    87  		if err := scr.runFile(t, filename); err != nil {
    88  			return err
    89  		}
    90  	}
    91  	return nil
    92  }
    93  func (scr *Script) runFile(t1 *testing.T, filename string) error {
    94  	err0 := error(nil)
    95  	name := filepath.Base(filename)
    96  	ok := t1.Run(name, func(t2 *testing.T) {
    97  		// running as a sub-test
    98  		scr.logf(t2, "script: %v", filename)
    99  
   100  		ar, err := txtar.ParseFile(filename)
   101  		if err != nil {
   102  			err0 = err // stop testing by returning an error
   103  			return
   104  		}
   105  
   106  		func() { // run in func to use defer inside
   107  			if scr.ScriptStart != nil {
   108  				if err := scr.ScriptStart(t2); err != nil {
   109  					t2.Fatal(err)
   110  				}
   111  			}
   112  			if scr.ScriptStop != nil {
   113  				defer func() {
   114  					if err := scr.ScriptStop(t2); err != nil {
   115  						t2.Fatal(err)
   116  					}
   117  				}()
   118  			}
   119  
   120  			if err := scr.runScript(t2, filename, ar); err != nil {
   121  				t2.Logf("FAIL: %v", err)
   122  				//t2.Fail()  // continues testing
   123  				t2.Fatal() // also seems to continue, need t1
   124  				t1.Fatal() // stop testing
   125  			}
   126  		}()
   127  	})
   128  	_ = ok
   129  	return err0
   130  }
   131  func (scr *Script) runScript(t *testing.T, filename string, ar *txtar.Archive) error {
   132  	// create working dir
   133  	// TODO: review, not working properly
   134  	//dir, err := os.MkdirTemp(t.TempDir(), "editor_testutil_work.*")
   135  	dir, err := os.MkdirTemp("", "editor_testutilscript*")
   136  	if err != nil {
   137  		return err
   138  	}
   139  	scr.workDir = dir
   140  	t.Setenv("WORK", scr.workDir)
   141  	scr.logf(t, "script_workdir: %v", scr.workDir)
   142  	defer func() {
   143  		if scr.Work {
   144  			scr.logf(t, "workDir not cleaned")
   145  		} else {
   146  			_ = os.RemoveAll(scr.workDir)
   147  		}
   148  	}()
   149  
   150  	// keep/restore current dir
   151  	keepDir, err := os.Getwd()
   152  	if err != nil {
   153  		return err
   154  	}
   155  	defer os.Chdir(keepDir)
   156  
   157  	// switch to working dir
   158  	if err := os.Chdir(scr.workDir); err != nil {
   159  		return err
   160  	}
   161  
   162  	// setup tmp dir in workdir for program to create its own tmp files
   163  	scriptTmpDir := filepath.Join(scr.workDir, "tmp")
   164  	t.Setenv("TMPDIR", scriptTmpDir)
   165  	if err := iout.MkdirAll(scriptTmpDir); err != nil {
   166  		return err
   167  	}
   168  
   169  	for _, f := range ar.Files {
   170  		if err := scr.writeToTmp(f.Name, f.Data); err != nil {
   171  			return err
   172  		}
   173  	}
   174  
   175  	// run script
   176  	rd := bytes.NewReader(ar.Comment)
   177  	scanner := bufio.NewScanner(rd)
   178  	line := 0
   179  	for scanner.Scan() {
   180  		line++
   181  		txt := strings.TrimSpace(scanner.Text())
   182  		// comments
   183  		if strings.HasPrefix(txt, "#") {
   184  			continue
   185  		}
   186  		// empty lines
   187  		if txt == "" {
   188  			continue
   189  		}
   190  		// as least an arg after empty lines check
   191  		args := scr.splitArgs(txt)
   192  
   193  		cmd, ok := scr.icmds[args[0]]
   194  		if !ok {
   195  			err := fmt.Errorf("cmd not found: %v", args[0])
   196  			return &lineError{filename, line, err}
   197  		}
   198  		scr.logf(t, "%v: %v", args[0], args[1:])
   199  		if err := cmd.Fn(t, args); err != nil {
   200  			return &lineError{filename, line, err}
   201  		}
   202  	}
   203  	if err := scanner.Err(); err != nil {
   204  		return &lineError{filename, line, err}
   205  	}
   206  	return nil
   207  }
   208  
   209  //----------
   210  
   211  func (scr *Script) splitArgs(s string) []string {
   212  	quoted := false
   213  	escape := false
   214  	a := strings.FieldsFunc(s, func(r rune) bool {
   215  		if r == '\\' {
   216  			escape = true
   217  			return false
   218  		}
   219  		if escape {
   220  			escape = false
   221  			return false
   222  		}
   223  		if r == '"' {
   224  			quoted = !quoted
   225  		}
   226  		return !quoted && r == ' '
   227  	})
   228  	return a
   229  }
   230  
   231  //----------
   232  
   233  func (scr *Script) collectOutput(t *testing.T, fn func() error) error {
   234  	stdout, stderr, err := CollectLog(t, fn)
   235  
   236  	scr.lastCmd.stdout = stdout
   237  	scr.lastCmd.stderr = stderr
   238  	scr.lastCmd.err = nil
   239  	if err != nil {
   240  		scr.lastCmd.err = []byte(err.Error())
   241  	}
   242  
   243  	return err
   244  }
   245  
   246  //----------
   247  
   248  func (scr *Script) writeToTmp(filename string, data []byte) error {
   249  	filename2 := filepath.Join(scr.workDir, filename)
   250  	return iout.MkdirAllWriteFile(filename2, data, 0o644)
   251  }
   252  
   253  //----------
   254  
   255  func (scr *Script) icExec(t *testing.T, args []string) error {
   256  	args = args[1:] // drop "exec"
   257  	if len(args) <= 0 {
   258  		return fmt.Errorf("expecting args, got %v", len(args))
   259  	}
   260  	ctx := context.Background()
   261  	ec := exec.CommandContext(ctx, args[0], args[1:]...)
   262  
   263  	//ec.Dir = // commented: dir set with os.Chdir previously
   264  
   265  	return scr.collectOutput(t, func() error {
   266  		// setup cmd stdout inside collectoutput
   267  		// TODO: stdin?
   268  		ec.Stdout = os.Stdout
   269  		ec.Stderr = os.Stderr
   270  
   271  		ci := osutil.NewCmdI(ec)
   272  		ci = osutil.NewSetSidCmd(ctx, ci)
   273  		ci = osutil.NewShellCmd(ci)
   274  		return osutil.RunCmdI(ci)
   275  	})
   276  }
   277  
   278  //----------
   279  
   280  func (scr *Script) icUCmd(t *testing.T, args []string) error {
   281  	args = args[1:] // drop "cmd"
   282  	cmd, ok := scr.ucmds[args[0]]
   283  	if !ok {
   284  		return fmt.Errorf("cmd not found: %v", args[0])
   285  	}
   286  	return scr.collectOutput(t, func() error {
   287  		return cmd.Fn(t, args)
   288  	})
   289  }
   290  
   291  //----------
   292  
   293  func (scr *Script) icContains(t *testing.T, args []string) error {
   294  	args = args[1:] // drop "contains"
   295  	if len(args) != 2 {
   296  		return fmt.Errorf("expecting 2 args, got %v", args)
   297  	}
   298  
   299  	data, ok := scr.lastCmdContent(args[0])
   300  	if !ok {
   301  		return fmt.Errorf("unknown content: %v", args[0])
   302  	}
   303  
   304  	// pattern
   305  	u, err := strconv.Unquote(args[1])
   306  	if err != nil {
   307  		return err
   308  	}
   309  	pattern := u
   310  
   311  	if !bytes.Contains(data, []byte(pattern)) {
   312  		//return fmt.Errorf("contains: no match:\npattern=[%v]\ndata=[%v]", pattern, string(data))
   313  		return fmt.Errorf("contains: no match")
   314  	}
   315  	return nil
   316  }
   317  
   318  //----------
   319  
   320  func (scr *Script) icSetEnv(t *testing.T, args []string) error {
   321  	args = args[1:] // drop "setenv"
   322  	if len(args) != 1 && len(args) != 2 {
   323  		return fmt.Errorf("expecting 1 or 2 args, got %v", args)
   324  	}
   325  	v := "" // allow setting to empty
   326  	if len(args) == 2 {
   327  		v = args[1]
   328  
   329  		// allow env expansion when setting env vars
   330  		v = os.Expand(v, os.Getenv)
   331  
   332  		// allow expansion of lastcmd
   333  		data, ok := scr.lastCmdContent(v)
   334  		if ok {
   335  			v = string(data)
   336  		}
   337  	}
   338  	t.Setenv(args[0], v)
   339  	return nil
   340  }
   341  
   342  //----------
   343  
   344  func (scr *Script) icFail(t *testing.T, args []string) error {
   345  	args = args[1:] // drop "fail"
   346  	if len(args) < 1 {
   347  		return fmt.Errorf("expecting at least 1 arg, got %v", args)
   348  	}
   349  	cmd, ok := scr.icmds[args[0]]
   350  	if !ok {
   351  		return fmt.Errorf("cmd not found: %v", args[0])
   352  	}
   353  	err := cmd.Fn(t, args)
   354  	if err == nil {
   355  		return fmt.Errorf("expected failure but got no error")
   356  	}
   357  	scr.logf(t, "fail ok: %v", err)
   358  	return nil
   359  }
   360  
   361  //----------
   362  
   363  func (scr *Script) icChangeDir(t *testing.T, args []string) error {
   364  	args = args[1:] // drop "cd"
   365  	if len(args) != 1 {
   366  		return fmt.Errorf("expecting 1 arg, got %v", args)
   367  	}
   368  	dir := args[0]
   369  	return os.Chdir(dir)
   370  }
   371  
   372  //----------
   373  
   374  func (scr *Script) lastCmdContent(name string) ([]byte, bool) {
   375  	switch name {
   376  	case "stdout":
   377  		return scr.lastCmd.stdout, true
   378  	case "stderr":
   379  		return scr.lastCmd.stderr, true
   380  	case "error":
   381  		return scr.lastCmd.err, true
   382  	}
   383  	return nil, false
   384  }
   385  
   386  //----------
   387  //----------
   388  //----------
   389  
   390  type ScriptCmd struct {
   391  	Name string
   392  	Fn   func(t *testing.T, args []string) error
   393  }
   394  
   395  func mapScriptCmds(w []*ScriptCmd) map[string]*ScriptCmd {
   396  	m := map[string]*ScriptCmd{}
   397  	for _, cmd := range w {
   398  		m[cmd.Name] = cmd
   399  	}
   400  	return m
   401  }
   402  
   403  //----------
   404  
   405  type lineError struct {
   406  	filename string
   407  	line     int
   408  	err      error
   409  }
   410  
   411  func (le *lineError) Error() string {
   412  	return fmt.Sprintf("%v:%v: %v", le.filename, le.line, le.err)
   413  }
   414  func (le *lineError) Is(err error) bool {
   415  	return errors.Is(le.err, err)
   416  }
   417  
   418  //----------
   419  //----------
   420  //----------