Skip to content

Commit

Permalink
Migrate knative.dev/hack/shell to knative.dev/pkg/test/shell (#2856)
Browse files Browse the repository at this point in the history
* 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
mgencur authored Oct 12, 2023
1 parent 948f5f5 commit 9051a45
Show file tree
Hide file tree
Showing 9 changed files with 749 additions and 0 deletions.
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

0 comments on commit 9051a45

Please sign in to comment.