-
Notifications
You must be signed in to change notification settings - Fork 331
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Migrate knative.dev/hack/shell to knative.dev/pkg/test/shell (#2856)
* Move shell package from knative.dev/hack under test/shell * Provide Streams for shell executor that writes to t.Log * Remove temporary comment * Fix style * Fix lint * Do not fail when writing to stderr * use own interface TestingT * Fix arguments for Logf * Fix comments * NewExecutor function requires TestingT and ProjectLocation * Move pkg/test/shell under pkg/test/upgrade/shell * ProjectLocation is always passed It is now a required argument for NewExecutor function * Remove redundant file
- Loading branch information
Showing
9 changed files
with
749 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
/* | ||
Copyright 2020 The Knative Authors | ||
Licensed under the Apache License, Version 2.0 (the "License"); | ||
you may not use this file except in compliance with the License. | ||
You may obtain a copy of the License at | ||
http://www.apache.org/licenses/LICENSE-2.0 | ||
Unless required by applicable law or agreed to in writing, software | ||
distributed under the License is distributed on an "AS IS" BASIS, | ||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
See the License for the specific language governing permissions and | ||
limitations under the License. | ||
*/ | ||
|
||
package shell_test | ||
|
||
import ( | ||
"strings" | ||
"testing" | ||
) | ||
|
||
type assertions struct { | ||
t *testing.T | ||
} | ||
|
||
func (a assertions) NoError(err error) { | ||
if err != nil { | ||
a.t.Error(err) | ||
} | ||
} | ||
|
||
func (a assertions) Contains(haystack, needle string) { | ||
if !strings.Contains(haystack, needle) { | ||
a.t.Errorf("wanted to \ncontain: %#v\n in: %#v", | ||
needle, haystack) | ||
} | ||
} | ||
|
||
func (a assertions) Equal(want, got string) { | ||
if got != want { | ||
a.t.Errorf("want: %#v\n got:%#v", want, got) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,191 @@ | ||
/* | ||
Copyright 2020 The Knative Authors | ||
Licensed under the Apache License, Version 2.0 (the "License"); | ||
you may not use this file except in compliance with the License. | ||
You may obtain a copy of the License at | ||
http://www.apache.org/licenses/LICENSE-2.0 | ||
Unless required by applicable law or agreed to in writing, software | ||
distributed under the License is distributed on an "AS IS" BASIS, | ||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
See the License for the specific language governing permissions and | ||
limitations under the License. | ||
*/ | ||
|
||
package shell | ||
|
||
import ( | ||
"bytes" | ||
"fmt" | ||
"os" | ||
"os/exec" | ||
"strings" | ||
"time" | ||
) | ||
|
||
const ( | ||
defaultLabelOut = "[OUT]" | ||
defaultLabelErr = "[ERR]" | ||
executeMode = 0700 | ||
) | ||
|
||
// NewExecutor creates a new executor. | ||
func NewExecutor(t TestingT, loc ProjectLocation, opts ...Option) Executor { | ||
config := &ExecutorConfig{ | ||
ProjectLocation: loc, | ||
Streams: testingTStreams(t), | ||
} | ||
for _, opt := range opts { | ||
opt(config) | ||
} | ||
configureDefaultValues(config) | ||
return &streamingExecutor{ | ||
ExecutorConfig: *config, | ||
} | ||
} | ||
|
||
// testingTStreams returns Streams which writes to test log. | ||
func testingTStreams(t TestingT) Streams { | ||
tWriter := testingWriter{t: t} | ||
return Streams{ | ||
Out: tWriter, | ||
Err: tWriter, | ||
} | ||
} | ||
|
||
// RunScript executes a shell script with args. | ||
func (s *streamingExecutor) RunScript(script Script, args ...string) error { | ||
cnt := script.scriptContent(s.ProjectLocation, args) | ||
return withTempScript(cnt, func(bin string) error { | ||
return stream(bin, s.ExecutorConfig, script.Label) | ||
}) | ||
} | ||
|
||
// RunFunction executes a shell function with args. | ||
func (s *streamingExecutor) RunFunction(fn Function, args ...string) error { | ||
cnt := fn.scriptContent(s.ProjectLocation, args) | ||
return withTempScript(cnt, func(bin string) error { | ||
return stream(bin, s.ExecutorConfig, fn.Label) | ||
}) | ||
} | ||
|
||
type streamingExecutor struct { | ||
ExecutorConfig | ||
} | ||
|
||
func configureDefaultValues(config *ExecutorConfig) { | ||
if config.LabelOut == "" { | ||
config.LabelOut = defaultLabelOut | ||
} | ||
if config.LabelErr == "" { | ||
config.LabelErr = defaultLabelErr | ||
} | ||
if config.Environ == nil { | ||
config.Environ = os.Environ() | ||
} | ||
if !config.SkipDate && config.DateFormat == "" { | ||
config.DateFormat = time.StampMilli | ||
} | ||
if config.PrefixFunc == nil { | ||
config.PrefixFunc = defaultPrefixFunc | ||
} | ||
} | ||
|
||
func stream(bin string, cfg ExecutorConfig, label string) error { | ||
c := exec.Command(bin) | ||
c.Env = cfg.Environ | ||
c.Stdout = NewPrefixer(cfg.Out, prefixFunc(StreamTypeOut, label, cfg)) | ||
c.Stderr = NewPrefixer(cfg.Err, prefixFunc(StreamTypeErr, label, cfg)) | ||
return c.Run() | ||
} | ||
|
||
func prefixFunc(st StreamType, label string, cfg ExecutorConfig) func() string { | ||
return func() string { | ||
return cfg.PrefixFunc(st, label, cfg) | ||
} | ||
} | ||
|
||
func defaultPrefixFunc(st StreamType, label string, cfg ExecutorConfig) string { | ||
sep := " " | ||
var buf []string | ||
if !cfg.SkipDate { | ||
dt := time.Now().Format(cfg.DateFormat) | ||
buf = append(buf, dt) | ||
} | ||
buf = append(buf, label) | ||
switch st { | ||
case StreamTypeOut: | ||
buf = append(buf, cfg.LabelOut) | ||
case StreamTypeErr: | ||
buf = append(buf, cfg.LabelErr) | ||
} | ||
return strings.Join(buf, sep) + sep | ||
} | ||
|
||
func withTempScript(contents string, fn func(bin string) error) error { | ||
tmpfile, err := os.CreateTemp("", "shellout-*.sh") | ||
if err != nil { | ||
return err | ||
} | ||
_, err = tmpfile.WriteString(contents) | ||
if err != nil { | ||
return err | ||
} | ||
err = tmpfile.Chmod(executeMode) | ||
if err != nil { | ||
return err | ||
} | ||
err = tmpfile.Close() | ||
if err != nil { | ||
return err | ||
} | ||
defer func() { | ||
// clean up | ||
_ = os.Remove(tmpfile.Name()) | ||
}() | ||
|
||
return fn(tmpfile.Name()) | ||
} | ||
|
||
func (fn *Function) scriptContent(location ProjectLocation, args []string) string { | ||
return fmt.Sprintf(`#!/usr/bin/env bash | ||
set -Eeuo pipefail | ||
cd "%s" | ||
source %s | ||
%s %s | ||
`, location.RootPath(), fn.ScriptPath, fn.FunctionName, quoteArgs(args)) | ||
} | ||
|
||
func (sc *Script) scriptContent(location ProjectLocation, args []string) string { | ||
return fmt.Sprintf(`#!/usr/bin/env bash | ||
set -Eeuo pipefail | ||
cd "%s" | ||
%s %s | ||
`, location.RootPath(), sc.ScriptPath, quoteArgs(args)) | ||
} | ||
|
||
func quoteArgs(args []string) string { | ||
quoted := make([]string, len(args)) | ||
for i, arg := range args { | ||
quoted[i] = "\"" + strings.ReplaceAll(arg, "\"", "\\\"") + "\"" | ||
} | ||
return strings.Join(quoted, " ") | ||
} | ||
|
||
func (w testingWriter) Write(p []byte) (n int, err error) { | ||
n = len(p) | ||
|
||
// Strip trailing newline because t.Log always adds one. | ||
p = bytes.TrimRight(p, "\n") | ||
|
||
w.t.Logf("%s", p) | ||
|
||
return n, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,145 @@ | ||
/* | ||
Copyright 2020 The Knative Authors | ||
Licensed under the Apache License, Version 2.0 (the "License"); | ||
you may not use this file except in compliance with the License. | ||
You may obtain a copy of the License at | ||
http://www.apache.org/licenses/LICENSE-2.0 | ||
Unless required by applicable law or agreed to in writing, software | ||
distributed under the License is distributed on an "AS IS" BASIS, | ||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
See the License for the specific language governing permissions and | ||
limitations under the License. | ||
*/ | ||
|
||
package shell_test | ||
|
||
import ( | ||
"bytes" | ||
"testing" | ||
|
||
"knative.dev/pkg/test/upgrade/shell" | ||
) | ||
|
||
func TestNewExecutor(t *testing.T) { | ||
assert := assertions{t: t} | ||
tests := []testcase{ | ||
helloWorldTestCase(t), | ||
abortTestCase(t), | ||
failExampleCase(t), | ||
} | ||
for _, tt := range tests { | ||
t.Run(tt.name, func(t *testing.T) { | ||
var outB, errB bytes.Buffer | ||
executor := shell.NewExecutor(t, tt.config.ProjectLocation, func(cfg *shell.ExecutorConfig) { | ||
cfg.Streams.Out = &outB | ||
cfg.Streams.Err = &errB | ||
}) | ||
err := tt.op(executor) | ||
if err != nil && !tt.wants.failed { | ||
t.Errorf("%s: \n got: %#v\nfailed: %#v", tt.name, err, tt.failed) | ||
} | ||
|
||
for _, wantOut := range tt.wants.outs { | ||
assert.Contains(outB.String(), wantOut) | ||
} | ||
for _, wantErr := range tt.wants.errs { | ||
assert.Contains(errB.String(), wantErr) | ||
} | ||
}) | ||
} | ||
} | ||
|
||
func TestExecutorDefaults(t *testing.T) { | ||
assert := assertions{t: t} | ||
loc, err := shell.NewProjectLocation("../../..") | ||
assert.NoError(err) | ||
exec := shell.NewExecutor(t, loc) | ||
err = exec.RunFunction(fn("true")) | ||
assert.NoError(err) | ||
} | ||
|
||
func helloWorldTestCase(t *testing.T) testcase { | ||
return testcase{ | ||
"echo Hello, World!", | ||
config(t, func(cfg *shell.ExecutorConfig) { | ||
cfg.SkipDate = true | ||
}), | ||
func(exec shell.Executor) error { | ||
return exec.RunFunction(fn("echo"), "Hello, World!") | ||
}, | ||
wants{ | ||
outs: []string{ | ||
"echo [OUT] Hello, World!", | ||
}, | ||
}, | ||
} | ||
} | ||
|
||
func abortTestCase(t *testing.T) testcase { | ||
return testcase{ | ||
"abort function", | ||
config(t, func(cfg *shell.ExecutorConfig) {}), | ||
func(exec shell.Executor) error { | ||
return exec.RunFunction(fn("abort"), "reason") | ||
}, | ||
wants{ | ||
failed: true, | ||
}, | ||
} | ||
} | ||
|
||
func failExampleCase(t *testing.T) testcase { | ||
return testcase{ | ||
"fail-example.sh", | ||
config(t, func(cfg *shell.ExecutorConfig) {}), | ||
func(exec shell.Executor) error { | ||
return exec.RunScript(shell.Script{ | ||
Label: "fail-example.sh", | ||
ScriptPath: "test/upgrade/shell/fail-example.sh", | ||
}, "expected err") | ||
}, | ||
wants{ | ||
failed: true, | ||
errs: []string{ | ||
"expected err", | ||
}, | ||
}, | ||
} | ||
} | ||
|
||
type wants struct { | ||
failed bool | ||
outs []string | ||
errs []string | ||
} | ||
|
||
type testcase struct { | ||
name string | ||
config shell.ExecutorConfig | ||
op func(exec shell.Executor) error | ||
wants | ||
} | ||
|
||
func config(t *testing.T, customize func(cfg *shell.ExecutorConfig)) shell.ExecutorConfig { | ||
assert := assertions{t: t} | ||
loc, err := shell.NewProjectLocation("../../..") | ||
assert.NoError(err) | ||
cfg := shell.ExecutorConfig{ | ||
ProjectLocation: loc, | ||
} | ||
customize(&cfg) | ||
return cfg | ||
} | ||
|
||
func fn(name string) shell.Function { | ||
return shell.Function{ | ||
Script: shell.Script{ | ||
Label: name, | ||
ScriptPath: "vendor/knative.dev/hack/library.sh", | ||
}, | ||
FunctionName: name, | ||
} | ||
} |
Oops, something went wrong.