2021-12-07 15:06:35 +02:00
//go:build integration
2020-07-20 18:07:08 +02:00
// +build integration
package main
import (
"bytes"
2020-07-21 11:15:46 +02:00
"fmt"
2020-07-20 18:07:08 +02:00
"io/ioutil"
"math/rand"
"os"
"path"
"strconv"
"strings"
"testing"
"time"
2021-12-07 15:06:35 +02:00
"github.com/SAP/jenkins-library/pkg/command"
"github.com/SAP/jenkins-library/pkg/log"
2020-07-20 18:07:08 +02:00
)
2020-07-22 15:48:46 +02:00
// 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:
// - Bash is available in the container
// - 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
2021-12-07 15:06:35 +02:00
Network string
2020-07-22 15:48:46 +02:00
}
2020-07-20 18:07:08 +02:00
// 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
2021-12-07 15:06:35 +02:00
Network string
2020-07-20 18:07:08 +02:00
ContainerName string
}
func givenThisContainer ( t * testing . T , bundle IntegrationTestDockerExecRunnerBundle ) IntegrationTestDockerExecRunner {
runner := command . Command { }
2020-07-22 15:48:46 +02:00
containerName := generateContainerName ( )
2020-07-20 18:07:08 +02:00
testRunner := IntegrationTestDockerExecRunner {
Runner : runner ,
Image : bundle . Image ,
User : bundle . User ,
Mounts : bundle . Mounts ,
2020-08-11 10:28:03 +02:00
Environment : bundle . Environment ,
2020-07-20 18:07:08 +02:00
Setup : bundle . Setup ,
2021-12-07 15:06:35 +02:00
Network : bundle . Network ,
2020-07-20 18:07:08 +02:00
ContainerName : containerName ,
}
wd , _ := os . Getwd ( )
localPiper := path . Join ( wd , ".." , "piper" )
if localPiper == "" {
t . Fatal ( "Could not locate piper binary to test" )
}
2020-07-21 11:15:46 +02:00
params := [ ] string { "run" , "--detach" , "-v" , localPiper + ":/piper" , "--name=" + testRunner . ContainerName }
if testRunner . User != "" {
params = append ( params , fmt . Sprintf ( "--user=%s" , testRunner . User ) )
2020-07-20 18:07:08 +02:00
}
2020-07-21 11:15:46 +02:00
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 := ioutil . TempDir ( "" , "piper-integration-test" )
if err != nil {
t . Fatal ( err )
}
2020-07-20 18:07:08 +02:00
2020-07-21 11:15:46 +02:00
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 ) )
}
2021-12-07 15:06:35 +02:00
2020-07-21 11:15:46 +02:00
if len ( testRunner . Environment ) > 0 {
for envVarName , envVarValue := range testRunner . Environment {
2020-08-11 10:28:03 +02:00
params = append ( params , "--env" , fmt . Sprintf ( "%s=%s" , envVarName , envVarValue ) )
2020-07-21 11:15:46 +02:00
}
2020-07-20 18:07:08 +02:00
}
2021-12-07 15:06:35 +02:00
if testRunner . Mounts != nil {
for src , dst := range testRunner . Mounts {
params = append ( params , "-v" , fmt . Sprintf ( "%s:%s" , src , dst ) )
}
}
if testRunner . Network != "" {
params = append ( params , "--network" , testRunner . Network )
}
2020-07-21 11:15:46 +02:00
params = append ( params , testRunner . Image , "sleep" , "2000" )
2020-07-20 18:07:08 +02:00
2020-07-21 11:15:46 +02:00
err := testRunner . Runner . RunExecutable ( "docker" , params ... )
2020-07-20 18:07:08 +02:00
if err != nil {
t . Fatalf ( "Starting test container has failed %s" , err )
}
2020-07-21 11:15:46 +02:00
if len ( bundle . TestDir ) > 0 {
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 )
}
}
2020-07-20 18:07:08 +02:00
for _ , scriptLine := range testRunner . Setup {
err := testRunner . Runner . RunExecutable ( "docker" , "exec" , testRunner . ContainerName , "/bin/bash" , "-c" , scriptLine )
if err != nil {
t . Fatalf ( "Running setup script in test container has failed %s" , err )
}
}
2020-07-22 15:48:46 +02:00
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" )
2020-07-20 18:07:08 +02:00
if err != nil {
t . Fatalf ( "Copying command wrapper to container has failed %s" , err )
}
2020-07-21 11:15:46 +02:00
err = testRunner . Runner . RunExecutable ( "docker" , "exec" , "-u=root" , testRunner . ContainerName , "chmod" , "+x" , "/piper-wrapper" )
2020-07-20 18:07:08 +02:00
if err != nil {
t . Fatalf ( "Making command wrapper in container executable has failed %s" , err )
}
2020-07-22 15:48:46 +02:00
err = testRunner . Runner . RunExecutable ( "docker" , "exec" , testRunner . ContainerName , "/bin/bash" , "/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 )
}
2020-07-20 18:07:08 +02:00
}
func ( d * IntegrationTestDockerExecRunner ) whenRunningPiperCommand ( command string , parameters ... string ) error {
2020-08-11 10:28:03 +02:00
args := [ ] string { "exec" , "--workdir" , "/project" , d . ContainerName , "/bin/bash" , "--login" , "/piper-wrapper" , "/piper" , command }
2020-07-20 18:07:08 +02:00
args = append ( args , parameters ... )
return d . Runner . RunExecutable ( "docker" , args ... )
}
2020-08-11 10:28:03 +02:00
func ( d * IntegrationTestDockerExecRunner ) runScriptInsideContainer ( script string ) error {
args := [ ] string { "exec" , "--workdir" , "/project" , d . ContainerName , "/bin/bash" , "--login" , "-c" , script }
return d . Runner . RunExecutable ( "docker" , args ... )
}
2020-07-20 18:07:08 +02:00
func ( d * IntegrationTestDockerExecRunner ) assertHasOutput ( t * testing . T , want string ) {
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 ( ) )
if err != nil {
t . Fatalf ( "Failed to get log output of container %s" , d . ContainerName )
}
if ! strings . Contains ( buffer . String ( ) , want ) {
t . Fatalf ( "Assertion has failed. Expected output %s in command output.\n%s" , want , buffer . String ( ) )
}
}
2020-07-21 11:15:46 +02:00
func ( d * IntegrationTestDockerExecRunner ) assertHasFile ( t * testing . T , want string ) {
err := d . Runner . RunExecutable ( "docker" , "exec" , d . ContainerName , "stat" , want )
if err != nil {
t . Fatalf ( "Assertion has failed. Expected file %s to exist in container. %s" , want , err )
}
}
2021-12-07 15:06:35 +02:00
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 )
}
}