File: exe.go

package info (click to toggle)
golang-github-rogpeppe-go-internal 1.14.1-2
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 1,220 kB
  • sloc: makefile: 6
file content (151 lines) | stat: -rw-r--r-- 5,130 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
// Copyright 2018 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package testscript

import (
	"io"
	"log"
	"os"
	"path/filepath"
	"runtime"
	"strings"
)

// TestingM is implemented by *testing.M. It's defined as an interface
// to allow testscript to co-exist with other testing frameworks
// that might also wish to call M.Run.
type TestingM interface {
	Run() int
}

// Deprecated: this option is no longer used.
func IgnoreMissedCoverage() {}

// Main should be called within a TestMain function to allow
// subcommands to be run in the testscript context.
// Main always calls [os.Exit], so it does not return back to the caller.
//
// The commands map holds the set of command names, each
// with an associated run function which may call os.Exit.
//
// When Run is called, these commands are installed as regular commands in the shell
// path, so can be invoked with "exec" or via any other command (for example a shell script).
//
// For backwards compatibility, the commands declared in the map can be run
// without "exec" - that is, "foo" will behave like "exec foo".
// This can be disabled with Params.RequireExplicitExec to keep consistency
// across test scripts, and to keep separate process executions explicit.
func Main(m TestingM, commands map[string]func()) {
	// Depending on os.Args[0], this is either the top-level execution of
	// the test binary by "go test", or the execution of one of the provided
	// commands via "foo" or "exec foo".

	cmdName := filepath.Base(os.Args[0])
	if runtime.GOOS == "windows" {
		cmdName = strings.TrimSuffix(cmdName, ".exe")
	}
	mainf := commands[cmdName]
	if mainf == nil {
		// Unknown command; this is just the top-level execution of the
		// test binary by "go test".
		os.Exit(testingMRun(m, commands))
	}
	// The command being registered is being invoked, so run it, then exit.
	os.Args[0] = cmdName
	mainf()
	os.Exit(0)
}

// testingMRun exists just so that we can use `defer`, given that [Main] above uses [os.Exit].
func testingMRun(m TestingM, commands map[string]func()) int {
	// Set up all commands in a directory, added in $PATH.
	tmpdir, err := os.MkdirTemp("", "testscript-main")
	if err != nil {
		log.Fatalf("could not set up temporary directory: %v", err)
	}
	defer func() {
		if err := os.RemoveAll(tmpdir); err != nil {
			log.Fatalf("cannot delete temporary directory: %v", err)
		}
	}()
	bindir := filepath.Join(tmpdir, "bin")
	if err := os.MkdirAll(bindir, 0o777); err != nil {
		log.Fatalf("could not set up PATH binary directory: %v", err)
	}
	os.Setenv("PATH", bindir+string(filepath.ListSeparator)+os.Getenv("PATH"))

	// We're not in a subcommand.
	for name := range commands {
		// Set up this command in the directory we added to $PATH.
		binfile := filepath.Join(bindir, name)
		if runtime.GOOS == "windows" {
			binfile += ".exe"
		}
		binpath, err := os.Executable()
		if err == nil {
			err = copyBinary(binpath, binfile)
		}
		if err != nil {
			log.Fatalf("could not set up %s in $PATH: %v", name, err)
		}
		scriptCmds[name] = func(ts *TestScript, neg bool, args []string) {
			if ts.params.RequireExplicitExec {
				ts.Fatalf("use 'exec %s' rather than '%s' (because RequireExplicitExec is enabled)", name, name)
			}
			ts.cmdExec(neg, append([]string{name}, args...))
		}
	}
	return m.Run()
}

// Deprecated: use [Main], as the only reason for returning exit codes
// was to collect full code coverage, which Go does automatically now:
// https://go.dev/blog/integration-test-coverage
func RunMain(m TestingM, commands map[string]func() int) (exitCode int) {
	commands2 := make(map[string]func(), len(commands))
	for name, fn := range commands {
		commands2[name] = func() { os.Exit(fn()) }
	}
	Main(m, commands2)
	// Main always calls os.Exit; we assume that all users of RunMain would have simply
	// called os.Exit with the returned exitCode as well, following the documentation.
	panic("unreachable")
}

// copyBinary makes a copy of a binary to a new location. It is used as part of
// setting up top-level commands in $PATH.
//
// It does not attempt to use symlinks for two reasons:
//
// First, some tools like cmd/go's -toolexec will be clever enough to realise
// when they're given a symlink, and they will use the symlink target for
// executing the program. This breaks testscript, as we depend on os.Args[0] to
// know what command to run.
//
// Second, symlinks might not be available on some environments, so we have to
// implement a "full copy" fallback anyway.
//
// However, we do try to use cloneFile, since that will probably work on most
// unix-like setups. Note that "go test" also places test binaries in the
// system's temporary directory, like we do.
func copyBinary(from, to string) error {
	if err := cloneFile(from, to); err == nil {
		return nil
	}
	writer, err := os.OpenFile(to, os.O_WRONLY|os.O_CREATE, 0o777)
	if err != nil {
		return err
	}
	defer writer.Close()

	reader, err := os.Open(from)
	if err != nil {
		return err
	}
	defer reader.Close()

	_, err = io.Copy(writer, reader)
	return err
}