mirror of
https://github.com/SAP/jenkins-library.git
synced 2024-12-12 10:55:20 +02:00
0ba4c2206c
* update all deprecated ioutil usages * forgotten changes * add missing imports * undo changing comment * add missing 'os' import * fix integration test --------- Co-authored-by: I557621 <jordi.van.liempt@sap.com> Co-authored-by: Gulom Alimov <gulomjon.alimov@sap.com>
304 lines
10 KiB
Go
304 lines
10 KiB
Go
//go:build integration
|
|
// +build integration
|
|
|
|
package main
|
|
|
|
import (
|
|
"archive/tar"
|
|
"bufio"
|
|
"bytes"
|
|
"fmt"
|
|
"io"
|
|
"math/rand"
|
|
"os"
|
|
"path"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/magiconair/properties/assert"
|
|
"github.com/pkg/errors"
|
|
|
|
"github.com/SAP/jenkins-library/pkg/command"
|
|
"github.com/SAP/jenkins-library/pkg/log"
|
|
)
|
|
|
|
// The functions in this file provide a convenient way to integration test the piper binary in docker containers.
|
|
// It follows the "given, when, then" approach for structuring tests.
|
|
// The general concept is that per test one container is started, one piper command is run and outcomes are asserted.
|
|
// Please note that so far this was only tested with debian/ubuntu based containers.
|
|
//
|
|
// Non-exhaustive list of assumptions those functions make:
|
|
// - the following commands are available in the container: sh, chown, sleep
|
|
// - If the option TestDir is not provided, the test project must be in the container image in the directory /project
|
|
|
|
// IntegrationTestDockerExecRunnerBundle is used to construct an instance of IntegrationTestDockerExecRunner
|
|
// This is what a test uses to specify the container it requires
|
|
type IntegrationTestDockerExecRunnerBundle struct {
|
|
Image string
|
|
User string
|
|
TestDir []string
|
|
Mounts map[string]string
|
|
Environment map[string]string
|
|
Setup []string
|
|
Network string
|
|
ExecNoLogin bool
|
|
}
|
|
|
|
// IntegrationTestDockerExecRunner keeps the state of an instance of a docker runner
|
|
type IntegrationTestDockerExecRunner struct {
|
|
// Runner is the ExecRunner to which all executions are forwarded in the end.
|
|
Runner command.Command
|
|
Image string
|
|
User string
|
|
TestDir []string
|
|
Mounts map[string]string
|
|
Environment map[string]string
|
|
Setup []string
|
|
Network string
|
|
ContainerName string
|
|
ExecNoLogin bool
|
|
}
|
|
|
|
func givenThisContainer(t *testing.T, bundle IntegrationTestDockerExecRunnerBundle) IntegrationTestDockerExecRunner {
|
|
|
|
runner := command.Command{}
|
|
containerName := generateContainerName()
|
|
|
|
testRunner := IntegrationTestDockerExecRunner{
|
|
Runner: runner,
|
|
Image: bundle.Image,
|
|
User: bundle.User,
|
|
Mounts: bundle.Mounts,
|
|
Environment: bundle.Environment,
|
|
Setup: bundle.Setup,
|
|
Network: bundle.Network,
|
|
ExecNoLogin: bundle.ExecNoLogin,
|
|
ContainerName: containerName,
|
|
}
|
|
|
|
wd, _ := os.Getwd()
|
|
localPiper := path.Join(wd, "..", "piper")
|
|
if localPiper == "" {
|
|
t.Fatal("Could not locate piper binary to test")
|
|
}
|
|
|
|
params := []string{"run", "--detach", "-v", localPiper + ":/piper", "--name=" + testRunner.ContainerName}
|
|
if testRunner.User != "" {
|
|
params = append(params, fmt.Sprintf("--user=%s", testRunner.User))
|
|
}
|
|
if len(bundle.TestDir) > 0 {
|
|
projectDir := path.Join(wd, path.Join(bundle.TestDir...))
|
|
// 1. Copy test files to a temp dir in order to avoid non-repeatable test executions because of changed state
|
|
// 2. Don't remove the temp dir to allow investigation of failed tests. Maybe add an option for cleaning it later?
|
|
tempDir, err := os.MkdirTemp("", "piper-integration-test")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
err = copyDir(projectDir, tempDir)
|
|
if err != nil {
|
|
t.Fatalf("Failed to copy files from %s into %s", projectDir, tempDir)
|
|
}
|
|
params = append(params, "-v", fmt.Sprintf("%s:/project", tempDir))
|
|
}
|
|
|
|
if len(testRunner.Environment) > 0 {
|
|
for envVarName, envVarValue := range testRunner.Environment {
|
|
params = append(params, "--env", fmt.Sprintf("%s=%s", envVarName, envVarValue))
|
|
}
|
|
}
|
|
|
|
if testRunner.Mounts != nil {
|
|
wd, _ := os.Getwd()
|
|
for src, dst := range testRunner.Mounts {
|
|
localSrc := path.Join(wd, src)
|
|
params = append(params, "-v", fmt.Sprintf("%s:%s", localSrc, dst))
|
|
}
|
|
}
|
|
|
|
if testRunner.Network != "" {
|
|
params = append(params, "--network", testRunner.Network)
|
|
}
|
|
params = append(params, testRunner.Image, "sleep", "2000")
|
|
|
|
err := testRunner.Runner.RunExecutable("docker", params...)
|
|
if err != nil {
|
|
t.Fatalf("Starting test container has failed %s", err)
|
|
}
|
|
|
|
if len(bundle.TestDir) > 0 && testRunner.User != "" {
|
|
err = testRunner.Runner.RunExecutable("docker", "exec", "-u=root", testRunner.ContainerName, "chown", "-R", testRunner.User, "/project")
|
|
if err != nil {
|
|
t.Fatalf("Chown /project has failed %s", err)
|
|
}
|
|
}
|
|
|
|
if err = testRunner.Runner.RunExecutable(
|
|
"docker", "exec", testRunner.ContainerName, "sh", "-c",
|
|
strings.Join(testRunner.Setup, "\n"),
|
|
); err != nil {
|
|
t.Fatalf("Running setup script in test container has failed %s", err)
|
|
}
|
|
|
|
setupPiperBinary(t, testRunner, localPiper)
|
|
|
|
return testRunner
|
|
}
|
|
|
|
// generateContainerName creates a name with a common prefix and a random number, so we can start a new container for each test method
|
|
// We don't rely on docker's random name generator for two reasons
|
|
// First, it is easier to save the name here compared to getting it from stdout
|
|
// Second, the common prefix allows batch stopping/deleting of containers if so desired
|
|
// The test code will not automatically delete containers as they might be useful for debugging
|
|
func generateContainerName() string {
|
|
var seededRand = rand.New(rand.NewSource(time.Now().UnixNano()))
|
|
return "piper-integration-test-" + strconv.Itoa(seededRand.Int())
|
|
}
|
|
|
|
// setupPiperBinary copies a wrapper script for calling the piper binary into the container and verifies that the piper binary is executable inside the container
|
|
// The wrapper script (piper-command-wrapper.sh) only calls the piper binary and redirects its output into a file
|
|
// The purpose of this is to capture piper's stdout/stderr in order to assert on the output
|
|
// This is not possible via "docker logs", cf https://github.com/moby/moby/issues/8662
|
|
func setupPiperBinary(t *testing.T, testRunner IntegrationTestDockerExecRunner, localPiper string) {
|
|
err := testRunner.Runner.RunExecutable("docker", "cp", "piper-command-wrapper.sh", testRunner.ContainerName+":/piper-wrapper")
|
|
if err != nil {
|
|
t.Fatalf("Copying command wrapper to container has failed %s", err)
|
|
}
|
|
err = testRunner.Runner.RunExecutable("docker", "exec", "-u=root", testRunner.ContainerName, "chmod", "+x", "/piper-wrapper")
|
|
if err != nil {
|
|
t.Fatalf("Making command wrapper in container executable has failed %s", err)
|
|
}
|
|
err = testRunner.Runner.RunExecutable("docker", "exec", testRunner.ContainerName, "/bin/sh", "/piper-wrapper", "/piper", "version")
|
|
if err != nil {
|
|
t.Fatalf("Running piper failed. "+
|
|
"Please check that '%s' is the correct binary, and is compiled for this configuration: 'GOOS=linux GOARCH=amd64'. Error text: %s", localPiper, err)
|
|
}
|
|
}
|
|
|
|
func (d *IntegrationTestDockerExecRunner) whenRunningPiperCommand(command string, parameters ...string) error {
|
|
args := []string{"exec", "--workdir", "/project", d.ContainerName, "/bin/sh"}
|
|
|
|
if !d.ExecNoLogin {
|
|
args = append(args, "-l")
|
|
}
|
|
|
|
args = append(args, "/piper-wrapper", "/piper", command)
|
|
err := d.Runner.RunExecutable("docker", append(args, parameters...)...)
|
|
if err != nil {
|
|
stdOut, outputErr := d.getPiperOutput()
|
|
if outputErr != nil {
|
|
return errors.Wrap(outputErr, "unable to get output after Piper command failure")
|
|
}
|
|
return errors.Wrapf(err, "piper output: \n%s", stdOut.String())
|
|
}
|
|
return err
|
|
}
|
|
|
|
func (d *IntegrationTestDockerExecRunner) runScriptInsideContainer(script string) error {
|
|
args := []string{"exec", "--workdir", "/project", d.ContainerName, "/bin/sh"}
|
|
|
|
if !d.ExecNoLogin {
|
|
args = append(args, "-l")
|
|
}
|
|
|
|
args = append(args, "-c", script)
|
|
return d.Runner.RunExecutable("docker", args...)
|
|
}
|
|
|
|
func (d *IntegrationTestDockerExecRunner) assertHasNoOutput(t *testing.T, inconsistencies ...string) {
|
|
count := len(inconsistencies)
|
|
buffer, err := d.getPiperOutput()
|
|
if err != nil {
|
|
t.Fatalf("Failed to get log output of container %s", d.ContainerName)
|
|
}
|
|
scanner := bufio.NewScanner(buffer)
|
|
for scanner.Scan() && (len(inconsistencies) != 0) {
|
|
for i, str := range inconsistencies {
|
|
if strings.Contains(scanner.Text(), str) {
|
|
inconsistencies = append(inconsistencies[:i], inconsistencies[i+1:]...)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
assert.Equal(t, len(inconsistencies), count, fmt.Sprintf(
|
|
"[assertHasNoOutput] Unexpected command output:\n%s\n%s\n", buffer.String(), strings.Join(inconsistencies, "\n")),
|
|
)
|
|
}
|
|
|
|
func (d *IntegrationTestDockerExecRunner) assertHasOutput(t *testing.T, consistencies ...string) {
|
|
buffer, err := d.getPiperOutput()
|
|
if err != nil {
|
|
t.Fatalf("Failed to get log output of container %s", d.ContainerName)
|
|
}
|
|
scanner := bufio.NewScanner(buffer)
|
|
for scanner.Scan() && (len(consistencies) != 0) {
|
|
for i, str := range consistencies {
|
|
if strings.Contains(scanner.Text(), str) {
|
|
consistencies = append(consistencies[:i], consistencies[i+1:]...)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
assert.Equal(t, len(consistencies), 0, fmt.Sprintf(
|
|
"[assertHasOutput] Unexpected command output:\n%s\n%s\n", buffer.String(), strings.Join(consistencies, "\n")),
|
|
)
|
|
}
|
|
|
|
func (d *IntegrationTestDockerExecRunner) getPiperOutput() (*bytes.Buffer, error) {
|
|
buffer := new(bytes.Buffer)
|
|
d.Runner.Stdout(buffer)
|
|
err := d.Runner.RunExecutable("docker", "exec", d.ContainerName, "cat", "/tmp/test-log.txt")
|
|
d.Runner.Stdout(log.Writer())
|
|
return buffer, err
|
|
}
|
|
|
|
func (d *IntegrationTestDockerExecRunner) assertHasFiles(t *testing.T, consistencies ...string) {
|
|
buffer := new(bytes.Buffer)
|
|
d.Runner.Stderr(buffer)
|
|
if d.Runner.RunExecutable(
|
|
"docker",
|
|
append(append(make([]string, 0), "exec", d.ContainerName, "stat"), consistencies...)...,
|
|
) != nil {
|
|
t.Fatalf("[assertHasFiles] Assertion has failed: %v", errors.New(buffer.String()))
|
|
}
|
|
}
|
|
|
|
func (d *IntegrationTestDockerExecRunner) assertFileContentEquals(t *testing.T, fileWant string, contentWant string) {
|
|
d.assertHasFiles(t, fileWant)
|
|
|
|
buffer := new(bytes.Buffer)
|
|
d.Runner.Stdout(buffer)
|
|
err := d.Runner.RunExecutable("docker", "cp", d.ContainerName+":/"+fileWant, "-")
|
|
if err != nil {
|
|
t.Fatalf("Copy file has failed. Expected file %s to exist in container. %s", fileWant, err)
|
|
}
|
|
|
|
tarReader := tar.NewReader(buffer)
|
|
header, err := tarReader.Next()
|
|
if err == io.EOF {
|
|
t.Fatal("Empty tar received")
|
|
}
|
|
if err != nil {
|
|
t.Fatalf("Cant read tar: %s", err)
|
|
}
|
|
if header.Typeflag != tar.TypeReg {
|
|
t.Fatalf("Expected a file, but received %c", header.Typeflag)
|
|
}
|
|
str := new(bytes.Buffer)
|
|
_, err = io.Copy(str, tarReader)
|
|
if err != nil {
|
|
t.Fatalf("unable to get tar file content: %s", err)
|
|
}
|
|
|
|
assert.Equal(t, str.String(), contentWant, fmt.Sprintf("Unexpected content of file '%s'", fileWant))
|
|
}
|
|
|
|
func (d *IntegrationTestDockerExecRunner) terminate(t *testing.T) {
|
|
err := d.Runner.RunExecutable("docker", "rm", "-f", d.ContainerName)
|
|
if err != nil {
|
|
t.Fatalf("Failed to terminate container '%s'", d.ContainerName)
|
|
}
|
|
}
|