Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Migrate knative.dev/hack/shell to knative.dev/pkg/test/shell #2856

Merged
merged 12 commits into from
Oct 12, 2023
45 changes: 45 additions & 0 deletions test/upgrade/shell/assertions_test.go
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)
}
}
191 changes: 191 additions & 0 deletions test/upgrade/shell/executor.go
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
}
145 changes: 145 additions & 0 deletions test/upgrade/shell/executor_test.go
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,
}
}
Loading
Loading