github.com/rigado/snapd@v2.42.5-go-mod+incompatible/testutil/exec.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2016 Canonical Ltd
     5   *
     6   * This program is free software: you can redistribute it and/or modify
     7   * it under the terms of the GNU General Public License version 3 as
     8   * published by the Free Software Foundation.
     9   *
    10   * This program is distributed in the hope that it will be useful,
    11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
    12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    13   * GNU General Public License for more details.
    14   *
    15   * You should have received a copy of the GNU General Public License
    16   * along with this program.  If not, see <http://www.gnu.org/licenses/>.
    17   *
    18   */
    19  
    20  package testutil
    21  
    22  import (
    23  	"bytes"
    24  	"fmt"
    25  	"io"
    26  	"io/ioutil"
    27  	"os"
    28  	"os/exec"
    29  	"path"
    30  	"path/filepath"
    31  	"strings"
    32  	"sync"
    33  
    34  	"gopkg.in/check.v1"
    35  )
    36  
    37  var shellcheckPath string
    38  
    39  func init() {
    40  	if p, err := exec.LookPath("shellcheck"); err == nil {
    41  		shellcheckPath = p
    42  	}
    43  }
    44  
    45  var (
    46  	shellchecked   = make(map[string]bool, 16)
    47  	shellcheckedMu sync.Mutex
    48  )
    49  
    50  func shellcheckSeenAlready(script string) bool {
    51  	shellcheckedMu.Lock()
    52  	defer shellcheckedMu.Unlock()
    53  	if shellchecked[script] {
    54  		return true
    55  	}
    56  	shellchecked[script] = true
    57  	return false
    58  }
    59  
    60  func maybeShellcheck(c *check.C, script string, wholeScript io.Reader) {
    61  	// MockCommand is used sometimes in SetUptTest, so it adds up
    62  	// even for the empty script, don't recheck the essentially same
    63  	// thing again and again!
    64  	if shellcheckSeenAlready(script) {
    65  		return
    66  	}
    67  	c.Logf("using shellcheck: %q", shellcheckPath)
    68  	if shellcheckPath == "" {
    69  		// no shellcheck, nothing to do
    70  		return
    71  	}
    72  	cmd := exec.Command(shellcheckPath, "-s", "bash", "-")
    73  	cmd.Stdin = wholeScript
    74  	out, err := cmd.CombinedOutput()
    75  	c.Check(err, check.IsNil, check.Commentf("shellcheck failed:\n%s", string(out)))
    76  }
    77  
    78  // MockCmd allows mocking commands for testing.
    79  type MockCmd struct {
    80  	binDir  string
    81  	exeFile string
    82  	logFile string
    83  }
    84  
    85  // The top of the script generate the output to capture the
    86  // command that was run and the arguments used. To support
    87  // mocking commands that need "\n" in their args (like zenity)
    88  // we use the following convention:
    89  // - generate \0 to separate args
    90  // - generate \0\0 to separate commands
    91  var scriptTpl = `#!/bin/bash
    92  printf "%%s" "$(basename "$0")" >> %[1]q
    93  printf '\0' >> %[1]q
    94  
    95  for arg in "$@"; do
    96       printf "%%s" "$arg" >> %[1]q
    97       printf '\0'  >> %[1]q
    98  done
    99  
   100  printf '\0' >> %[1]q
   101  %s
   102  `
   103  
   104  // MockCommand adds a mocked command. If the basename argument is a command
   105  // it is added to PATH. If it is an absolute path it is just created there.
   106  // the caller is responsible for the cleanup in this case.
   107  //
   108  // The command logs all invocations to a dedicated log file. If script is
   109  // non-empty then it is used as is and the caller is responsible for how the
   110  // script behaves (exit code and any extra behavior). If script is empty then
   111  // the command exits successfully without any other side-effect.
   112  func MockCommand(c *check.C, basename, script string) *MockCmd {
   113  	var wholeScript bytes.Buffer
   114  	var binDir, exeFile, logFile string
   115  	if filepath.IsAbs(basename) {
   116  		binDir = filepath.Dir(basename)
   117  		exeFile = basename
   118  		logFile = basename + ".log"
   119  	} else {
   120  		binDir = c.MkDir()
   121  		exeFile = path.Join(binDir, basename)
   122  		logFile = path.Join(binDir, basename+".log")
   123  		os.Setenv("PATH", binDir+":"+os.Getenv("PATH"))
   124  	}
   125  	fmt.Fprintf(&wholeScript, scriptTpl, logFile, script)
   126  	err := ioutil.WriteFile(exeFile, wholeScript.Bytes(), 0700)
   127  	if err != nil {
   128  		panic(err)
   129  	}
   130  
   131  	maybeShellcheck(c, script, &wholeScript)
   132  
   133  	return &MockCmd{binDir: binDir, exeFile: exeFile, logFile: logFile}
   134  }
   135  
   136  // Also mock this command, using the same bindir and log.
   137  // Useful when you want to check the ordering of things.
   138  func (cmd *MockCmd) Also(basename, script string) *MockCmd {
   139  	exeFile := path.Join(cmd.binDir, basename)
   140  	err := ioutil.WriteFile(exeFile, []byte(fmt.Sprintf(scriptTpl, cmd.logFile, script)), 0700)
   141  	if err != nil {
   142  		panic(err)
   143  	}
   144  	return &MockCmd{binDir: cmd.binDir, exeFile: exeFile, logFile: cmd.logFile}
   145  }
   146  
   147  // Restore removes the mocked command from PATH
   148  func (cmd *MockCmd) Restore() {
   149  	entries := strings.Split(os.Getenv("PATH"), ":")
   150  	for i, entry := range entries {
   151  		if entry == cmd.binDir {
   152  			entries = append(entries[:i], entries[i+1:]...)
   153  			break
   154  		}
   155  	}
   156  	os.Setenv("PATH", strings.Join(entries, ":"))
   157  }
   158  
   159  // Calls returns a list of calls that were made to the mock command.
   160  // of the form:
   161  // [][]string{
   162  //     {"cmd", "arg1", "arg2"}, // first invocation of "cmd"
   163  //     {"cmd", "arg1", "arg2"}, // second invocation of "cmd"
   164  // }
   165  func (cmd *MockCmd) Calls() [][]string {
   166  	raw, err := ioutil.ReadFile(cmd.logFile)
   167  	if os.IsNotExist(err) {
   168  		return nil
   169  	}
   170  	if err != nil {
   171  		panic(err)
   172  	}
   173  	logContent := strings.TrimSuffix(string(raw), "\000")
   174  
   175  	allCalls := [][]string{}
   176  	calls := strings.Split(logContent, "\000\000")
   177  	for _, call := range calls {
   178  		call = strings.TrimSuffix(call, "\000")
   179  		allCalls = append(allCalls, strings.Split(call, "\000"))
   180  	}
   181  	return allCalls
   182  }
   183  
   184  // ForgetCalls purges the list of calls made so far
   185  func (cmd *MockCmd) ForgetCalls() {
   186  	err := os.Remove(cmd.logFile)
   187  	if os.IsNotExist(err) {
   188  		return
   189  	}
   190  	if err != nil {
   191  		panic(err)
   192  	}
   193  }
   194  
   195  // BinDir returns the location of the directory holding overridden commands.
   196  func (cmd *MockCmd) BinDir() string {
   197  	return cmd.binDir
   198  }
   199  
   200  // Exe return the full path of the mock binary
   201  func (cmd *MockCmd) Exe() string {
   202  	return filepath.Join(cmd.exeFile)
   203  }