Skip to content

Commit

Permalink
chore: added basic cli unit tests
Browse files Browse the repository at this point in the history
Signed-off-by: Ben Meier <[email protected]>
  • Loading branch information
astromechza committed May 10, 2024
1 parent 88f20d4 commit 47edb1d
Show file tree
Hide file tree
Showing 5 changed files with 359 additions and 6 deletions.
12 changes: 6 additions & 6 deletions internal/provisioners/default/zz-default.provisioners.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@
- uri: template://example-provisioners/example-provisioner
# (Required) Which resource type to match
type: example-provisioner-resource
# (Optional) Which 'class' of the resource. Blank will match any class, a non-empty value like 'default' will match
# (Optional) Which 'class' of the resource. Null will match any class, a non-empty value like 'default' will match
# only resources of that class.
class: ""
# (Optional) The exact resource id to match. Blank will match any resource, a non-empty value will only match
class: null
# (Optional) The exact resource id to match. Null will match any resource, a non-empty value will only match
# the resource with exact same id.
id: ""
id: null
# (Optional) The init template sets the initial context values on each provision request. This is a text template
# that must evaluate to a YAML/JSON key-value map.
init: |
Expand All @@ -21,8 +21,8 @@
# state and the init context. Like init, this evaluates to a YAML/JSON object. This is the template that allows
# state to be stored between each generate call.
state: |
state-key: {{ .Init.key }} # will copy the value from init
state-key2: {{ default 0 .State.state-key2 | add 1 }} # will increment on each provision attempt
stateKey: {{ .Init.key }} # will copy the value from init
stateKey2: {{ default 0 .State.stateKey2 | add 1 }} # will increment on each provision attempt
# (Optional) The shared state template is like state, but is a key-value structure shared between all resources.
# This can be used to coordinate shared resources and state between resources of the same or related types.
shared: |
Expand Down
2 changes: 2 additions & 0 deletions main_generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,8 @@ manifests. All resources and links between Workloads will be resolved and provis
container.Image = v
slog.Info(fmt.Sprintf("Set container image for container '%s' to %s from --%s", containerName, v, generateCmdImageFlag))
workload.Containers[containerName] = container
} else {
return errors.Errorf("failed to convert '%s' because container '%s' has no image and --image was not provided", arg, containerName)
}
}
}
Expand Down
152 changes: 152 additions & 0 deletions main_generate_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
// Copyright 2024 Humanitec
//
// 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 main

import (
"context"
"os"
"path/filepath"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/score-spec/score-k8s/internal/project"
)

func changeToDir(t *testing.T, dir string) string {
t.Helper()
wd, _ := os.Getwd()
require.NoError(t, os.Chdir(dir))
t.Cleanup(func() {
require.NoError(t, os.Chdir(wd))
})
return dir
}

func changeToTempDir(t *testing.T) string {
return changeToDir(t, t.TempDir())
}

func TestGenerateWithoutInit(t *testing.T) {
_ = changeToTempDir(t)
stdout, _, err := executeAndResetCommand(context.Background(), rootCmd, []string{"generate"})
assert.EqualError(t, err, "state directory does not exist, please run \"score-k8s init\" first")
assert.Equal(t, "", stdout)
}

func TestGenerateWithoutScoreFiles(t *testing.T) {
_ = changeToTempDir(t)
stdout, _, err := executeAndResetCommand(context.Background(), rootCmd, []string{"init"})
assert.NoError(t, err)
assert.Equal(t, "", stdout)
stdout, _, err = executeAndResetCommand(context.Background(), rootCmd, []string{"generate"})
assert.EqualError(t, err, "Project is empty, please add a score file")
assert.Equal(t, "", stdout)
}

func TestInitAndGenerateWithBadFile(t *testing.T) {
td := changeToTempDir(t)
stdout, _, err := executeAndResetCommand(context.Background(), rootCmd, []string{"init"})
assert.NoError(t, err)
assert.Equal(t, "", stdout)

assert.NoError(t, os.WriteFile(filepath.Join(td, "thing"), []byte(`"blah"`), 0644))

stdout, _, err = executeAndResetCommand(context.Background(), rootCmd, []string{"generate", "thing"})
assert.EqualError(t, err, "failed to decode input score file: thing: yaml: unmarshal errors:\n line 1: cannot unmarshal !!str `blah` into map[string]interface {}")
assert.Equal(t, "", stdout)
}

func TestInitAndGenerateWithBadScore(t *testing.T) {
td := changeToTempDir(t)
stdout, _, err := executeAndResetCommand(context.Background(), rootCmd, []string{"init"})
assert.NoError(t, err)
assert.Equal(t, "", stdout)

assert.NoError(t, os.WriteFile(filepath.Join(td, "thing"), []byte(`{}`), 0644))

stdout, _, err = executeAndResetCommand(context.Background(), rootCmd, []string{"generate", "thing"})
assert.EqualError(t, err, "invalid score file: thing: jsonschema: '' does not validate with https://score.dev/schemas/score#/required: missing properties: 'apiVersion', 'metadata', 'containers'")
assert.Equal(t, "", stdout)
}

func TestInitAndGenerate_with_sample(t *testing.T) {
td := changeToTempDir(t)
stdout, _, err := executeAndResetCommand(context.Background(), rootCmd, []string{"init"})
require.NoError(t, err)
assert.Equal(t, "", stdout)

// write overrides file
assert.NoError(t, os.WriteFile(filepath.Join(td, "overrides.yaml"), []byte(`{"resources": {"foo": {"type": "example-provisioner-resource"}}}`), 0644))
// generate
stdout, _, err = executeAndResetCommand(context.Background(), rootCmd, []string{
"generate", "-o", "manifests.yaml",
"--overrides-file", "overrides.yaml",
"--override-property", "containers.main.variables.THING=${resources.foo.plaintext}",
"--", "score.yaml",
})
require.NoError(t, err)
assert.Equal(t, "", stdout)
raw, err := os.ReadFile(filepath.Join(td, "manifests.yaml"))
assert.NoError(t, err)
assert.Contains(t, string(raw), "\nkind: ConfigMap\n")
assert.Contains(t, string(raw), "\nkind: Service\n")
assert.Contains(t, string(raw), "\nkind: Deployment\n")

// check that state was persisted
sd, ok, err := project.LoadStateDirectory(td)
assert.NoError(t, err)
assert.True(t, ok)
assert.Equal(t, "score.yaml", *sd.State.Workloads["example"].File)
assert.Len(t, sd.State.Workloads, 1)
assert.Len(t, sd.State.Resources, 1)
}

func TestInitAndGenerate_with_image_override(t *testing.T) {
td := changeToTempDir(t)
stdout, _, err := executeAndResetCommand(context.Background(), rootCmd, []string{"init"})
assert.NoError(t, err)
assert.Equal(t, "", stdout)

// write new score file
assert.NoError(t, os.WriteFile(filepath.Join(td, "score.yaml"), []byte(`
apiVersion: score.dev/v1b1
metadata:
name: example
containers:
example:
image: .
`), 0644))

t.Run("generate but fail due to missing override", func(t *testing.T) {
stdout, _, err = executeAndResetCommand(context.Background(), rootCmd, []string{
"generate", "-o", "manifests.yaml", "--", "score.yaml",
})
assert.EqualError(t, err, "failed to convert 'score.yaml' because container 'example' has no image and --image was not provided")
})

t.Run("generate with image", func(t *testing.T) {
// generate with image
stdout, _, err = executeAndResetCommand(context.Background(), rootCmd, []string{
"generate", "-o", "manifests.yaml", "--image", "busybox:latest", "--", "score.yaml",
})
assert.NoError(t, err)
assert.Equal(t, "", stdout)
raw, err := os.ReadFile(filepath.Join(td, "manifests.yaml"))
assert.NoError(t, err)
assert.Contains(t, string(raw), "---\napiVersion: apps/v1\nkind: Deployment\n")
})
}
122 changes: 122 additions & 0 deletions main_init_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
// Copyright 2024 Humanitec
//
// 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 main

import (
"context"
"os"
"path/filepath"
"strings"
"testing"

"github.com/score-spec/score-go/framework"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/score-spec/score-k8s/internal/project"
)

func TestInitNominal(t *testing.T) {
td := t.TempDir()

wd, _ := os.Getwd()
require.NoError(t, os.Chdir(td))
defer func() {
require.NoError(t, os.Chdir(wd))
}()

stdout, stderr, err := executeAndResetCommand(context.Background(), rootCmd, []string{"init"})
assert.NoError(t, err)
assert.Equal(t, "", stdout)
assert.NotEqual(t, "", strings.TrimSpace(stderr))

stdout, stderr, err = executeAndResetCommand(context.Background(), rootCmd, []string{"generate", "score.yaml"})
assert.NoError(t, err)
assert.Equal(t, ``, stdout)
assert.NotEqual(t, "", strings.TrimSpace(stderr))

sd, ok, err := project.LoadStateDirectory(".")
assert.NoError(t, err)
if assert.True(t, ok) {
assert.Equal(t, project.DefaultRelativeStateDirectory, sd.Path)
assert.Len(t, sd.State.Workloads, 1)
assert.Equal(t, map[framework.ResourceUid]framework.ScoreResourceState[project.ResourceExtras]{}, sd.State.Resources)
assert.Equal(t, map[string]interface{}{}, sd.State.SharedState)
}
}

func TestInitNoSample(t *testing.T) {
td := t.TempDir()

wd, _ := os.Getwd()
require.NoError(t, os.Chdir(td))
defer func() {
require.NoError(t, os.Chdir(wd))
}()

stdout, stderr, err := executeAndResetCommand(context.Background(), rootCmd, []string{"init", "--no-sample"})
assert.NoError(t, err)
assert.Equal(t, "", stdout)
assert.NotEqual(t, "", strings.TrimSpace(stderr))

_, err = os.Stat("score.yaml")
assert.ErrorIs(t, err, os.ErrNotExist)
}

func TestInitNominal_run_twice(t *testing.T) {
td := t.TempDir()

wd, _ := os.Getwd()
require.NoError(t, os.Chdir(td))
defer func() {
require.NoError(t, os.Chdir(wd))
}()

// first init
stdout, stderr, err := executeAndResetCommand(context.Background(), rootCmd, []string{"init", "--file", "score2.yaml"})
assert.NoError(t, err)
assert.Equal(t, "", stdout)
assert.NotEqual(t, "", strings.TrimSpace(stderr))

// check default provisioners exists and overwrite it with an empty array
dpf, err := os.Stat(filepath.Join(td, ".score-k8s", "zz-default.provisioners.yaml"))
assert.NoError(t, err)
assert.NoError(t, os.WriteFile(filepath.Join(td, ".score-k8s", dpf.Name()), []byte("[]"), 0644))

// init again
stdout, stderr, err = executeAndResetCommand(context.Background(), rootCmd, []string{"init"})
assert.NoError(t, err)
assert.Equal(t, "", stdout)
assert.NotEqual(t, "", strings.TrimSpace(stderr))

// verify that default provisioners was not overwritten again
dpf, err = os.Stat(filepath.Join(td, ".score-k8s", dpf.Name()))
assert.NoError(t, err)
assert.Equal(t, 2, int(dpf.Size()))

_, err = os.Stat("score.yaml")
assert.NoError(t, err)
_, err = os.Stat("score2.yaml")
assert.NoError(t, err)

sd, ok, err := project.LoadStateDirectory(".")
assert.NoError(t, err)
if assert.True(t, ok) {
assert.Equal(t, project.DefaultRelativeStateDirectory, sd.Path)
assert.Equal(t, map[string]framework.ScoreWorkloadState[framework.NoExtras]{}, sd.State.Workloads)
assert.Equal(t, map[framework.ResourceUid]framework.ScoreResourceState[project.ResourceExtras]{}, sd.State.Resources)
assert.Equal(t, map[string]interface{}{}, sd.State.SharedState)
}
}
77 changes: 77 additions & 0 deletions main_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
// Copyright 2024 Humanitec
//
// 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 main

import (
"bytes"
"context"
"regexp"
"testing"

"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/stretchr/testify/assert"
)

// executeAndResetCommand is a test helper that runs and then resets a command for executing in another test.
func executeAndResetCommand(ctx context.Context, cmd *cobra.Command, args []string) (string, string, error) {
beforeOut, beforeErr := cmd.OutOrStdout(), cmd.ErrOrStderr()
defer func() {
cmd.SetOut(beforeOut)
cmd.SetErr(beforeErr)
// also have to remove completion commands which get auto added and bound to an output buffer
for _, command := range cmd.Commands() {
if command.Name() == "completion" {
cmd.RemoveCommand(command)
break
}
}
}()

nowOut, nowErr := new(bytes.Buffer), new(bytes.Buffer)
cmd.SetOut(nowOut)
cmd.SetErr(nowErr)
cmd.SetArgs(args)
subCmd, err := cmd.ExecuteContextC(ctx)
if subCmd != nil {
subCmd.SetOut(nil)
subCmd.SetErr(nil)
subCmd.SetContext(nil)
subCmd.SilenceUsage = false
subCmd.Flags().VisitAll(func(f *pflag.Flag) {
if f.Value.Type() == "stringArray" {
_ = f.Value.(pflag.SliceValue).Replace(nil)
} else {
_ = f.Value.Set(f.DefValue)
}
})
}
return nowOut.String(), nowErr.String(), err
}

func TestRootVersion(t *testing.T) {
stdout, stderr, err := executeAndResetCommand(context.Background(), rootCmd, []string{"--version"})
assert.NoError(t, err)
pattern := regexp.MustCompile(`^score-k8s 0.0.0 \(build: \S+, sha: \S+\)\n$`)
assert.Truef(t, pattern.MatchString(stdout), "%s does not match: '%s'", pattern.String(), stdout)
assert.Equal(t, "", stderr)
}

func TestRootUnknown(t *testing.T) {
stdout, stderr, err := executeAndResetCommand(context.Background(), rootCmd, []string{"unknown"})
assert.EqualError(t, err, "unknown command \"unknown\" for \"score-k8s\"")
assert.Equal(t, "", stdout)
assert.Equal(t, "", stderr)
}

0 comments on commit 47edb1d

Please sign in to comment.