github.com/mgoltzsche/ctnr@v0.7.1-alpha/image/builder/dockerfile/dockerfile_test.go (about)

     1  package dockerfile
     2  
     3  import (
     4  	"fmt"
     5  	"io/ioutil"
     6  	"log"
     7  	"os"
     8  	"path/filepath"
     9  	"sort"
    10  	"strconv"
    11  	"strings"
    12  	"testing"
    13  
    14  	"github.com/mgoltzsche/ctnr/pkg/idutils"
    15  	"github.com/opencontainers/go-digest"
    16  	"github.com/pkg/errors"
    17  	"github.com/stretchr/testify/assert"
    18  	"github.com/stretchr/testify/require"
    19  )
    20  
    21  func TestDockerfileApply(t *testing.T) {
    22  	files, err := filepath.Glob("testfiles/*.test")
    23  	require.NoError(t, err)
    24  	applyStageTested := false
    25  Files:
    26  	for _, file := range files {
    27  		fmt.Println("CASE ", file)
    28  		efile, err := os.Open(file[0:len(file)-4] + "expected")
    29  		require.NoError(t, err)
    30  		defer efile.Close()
    31  		b, err := ioutil.ReadAll(efile)
    32  		require.NoError(t, err, file)
    33  		expected := strings.TrimSpace(string(b))
    34  		expectedLinesBr := strings.Split(expected, "\n")
    35  		expectedLines := make([]string, 0, len(expectedLinesBr))
    36  		for _, eline := range expectedLinesBr {
    37  			if eline != "" {
    38  				expectedLines = append(expectedLines, eline)
    39  			}
    40  		}
    41  		testee := newTestee(t, file)
    42  		require.NoError(t, err, file)
    43  		mock := mockBuilder{returnErr: -1}
    44  		err = testee.Apply(&mock)
    45  		require.NoError(t, err)
    46  		for i, eline := range expectedLines {
    47  			aline := ""
    48  			if len(mock.ops) > i {
    49  				aline = mock.ops[i]
    50  			}
    51  			if eline != aline {
    52  				t.Errorf("%s: line %d not equal:\n  expected: %s\n  received: %s", filepath.Base(file), i, eline, aline)
    53  				continue Files
    54  			}
    55  		}
    56  		if len(expectedLines) < len(mock.ops) {
    57  			t.Errorf("%s: testee did unexpected tailing operation: %s", filepath.Base(file), mock.ops[len(expectedLines)])
    58  		}
    59  
    60  		// Test error handling
    61  		returnErr := 0
    62  		lastOpCount := 0
    63  		for {
    64  			testee = newTestee(t, file)
    65  			mock = mockBuilder{returnErr: returnErr}
    66  			err = testee.Apply(&mock)
    67  			if mock.returnCount == lastOpCount {
    68  				break
    69  			}
    70  			if mock.returnCount != lastOpCount+1 {
    71  				t.Errorf("%s: builder error not handled in %q", filepath.Base(file), mock.ops[len(mock.ops)-1])
    72  				break
    73  			}
    74  			if err == nil {
    75  				t.Errorf("%s: builder error not returned in %q", filepath.Base(file), mock.ops[len(mock.ops)-1])
    76  				break
    77  			}
    78  			lastOpCount = mock.returnCount
    79  			returnErr += 1
    80  		}
    81  		if lastOpCount < 2 {
    82  			t.Errorf("%s: test failed too early on builder error (or case contains <2 instructions)", file)
    83  		}
    84  
    85  		// Test single stage execution
    86  		if strings.Contains(file, "multistage") {
    87  			applyStageTested = true
    88  			expectedOps := mock.ops[mock.stage2OpOffset:mock.stage6OpOffset]
    89  			//panic(fmt.Sprintf("%d %s", mock.stage2OpOffset, strings.Join(expectedOps, "\n")))
    90  			testee := newTestee(t, file)
    91  			require.NoError(t, err, file)
    92  			mock = mockBuilder{returnErr: -1, stageCount: 1}
    93  			err = testee.Target("slim")
    94  			require.NoError(t, err, file)
    95  			err = testee.Apply(&mock)
    96  			require.NoError(t, err, file)
    97  			if !assert.Equal(t, expectedOps, mock.ops, filepath.Base(file)+": apply slim stage") {
    98  				t.FailNow()
    99  			}
   100  		}
   101  	}
   102  	if !applyStageTested {
   103  		t.Errorf("ApplyStage() has not been tested")
   104  	}
   105  }
   106  
   107  func newTestee(t *testing.T, file string) *DockerfileBuilder {
   108  	args := map[string]string{
   109  		"argp": "pval",
   110  	}
   111  	contents, err := ioutil.ReadFile(file)
   112  	require.NoError(t, err)
   113  	r, err := LoadDockerfile(contents, "./ctx", args, log.New(os.Stderr, "warn: "+file+":", 0))
   114  	require.NoError(t, err)
   115  	return r
   116  }
   117  
   118  type mockBuilder struct {
   119  	ops            []string
   120  	returnErr      int
   121  	returnCount    int
   122  	stageCount     int
   123  	stage2OpOffset int
   124  	stage6OpOffset int
   125  }
   126  
   127  func (s *mockBuilder) err() (err error) {
   128  	if s.returnCount == s.returnErr {
   129  		err = errors.New("expected error")
   130  	}
   131  	s.returnCount++
   132  	return
   133  }
   134  
   135  func (s *mockBuilder) add(op string) {
   136  	s.ops = append(s.ops, op)
   137  }
   138  
   139  func (s *mockBuilder) Image() digest.Digest {
   140  	return digest.Digest("stage" + strconv.Itoa(s.stageCount-1) + "-image")
   141  }
   142  
   143  func (s *mockBuilder) AddEnv(e map[string]string) error {
   144  	s.add("ENV " + mapToString(e))
   145  	return s.err()
   146  }
   147  
   148  func (s *mockBuilder) AddExposedPorts(p []string) error {
   149  	s.add("EXPOSE " + strings.Join(p, " "))
   150  	return s.err()
   151  }
   152  
   153  func (s *mockBuilder) AddLabels(l map[string]string) error {
   154  	s.add("LABEL " + mapToString(l))
   155  	return s.err()
   156  }
   157  
   158  func (s *mockBuilder) AddVolumes(v []string) error {
   159  	s.add("VOLUME " + sliceToString(v))
   160  	return s.err()
   161  }
   162  
   163  func (s *mockBuilder) AddFiles(srcDir string, srcPattern []string, dest string, user *idutils.User) error {
   164  	u := "nil"
   165  	if user != nil {
   166  		u = user.String()
   167  	}
   168  	s.add(fmt.Sprintf("ADD dir=%q %s %q %s", srcDir, sliceToString(srcPattern), dest, u))
   169  	return s.err()
   170  }
   171  
   172  func (s *mockBuilder) CopyFiles(srcDir string, srcPattern []string, dest string, user *idutils.User) error {
   173  	u := "nil"
   174  	if user != nil {
   175  		u = user.String()
   176  	}
   177  	s.add(fmt.Sprintf("COPY dir=%q %s %q %s", srcDir, sliceToString(srcPattern), dest, u))
   178  	return s.err()
   179  }
   180  
   181  func (s *mockBuilder) CopyFilesFromImage(srcImage string, srcPattern []string, dest string, user *idutils.User) error {
   182  	u := "nil"
   183  	if user != nil {
   184  		u = user.String()
   185  	}
   186  	s.add(fmt.Sprintf("COPY image=%q %s %q %s", srcImage, sliceToString(srcPattern), dest, u))
   187  	return s.err()
   188  }
   189  
   190  func (s *mockBuilder) FromImage(name string) error {
   191  	s.add("FROM " + name)
   192  	s.stageCount++
   193  	if s.stageCount == 2 {
   194  		s.stage2OpOffset = len(s.ops) - 1
   195  	}
   196  	if s.stageCount == 6 {
   197  		s.stage6OpOffset = len(s.ops) - 1
   198  	}
   199  	return s.err()
   200  }
   201  
   202  func (s *mockBuilder) Run(args []string, addEnv map[string]string) error {
   203  	s.add("RUN " + strings.TrimSpace(mapToString(addEnv)+" "+sliceToString(args)))
   204  	return s.err()
   205  }
   206  
   207  func (s *mockBuilder) SetAuthor(a string) error {
   208  	s.add("AUTHOR " + strconv.Quote(a))
   209  	return s.err()
   210  }
   211  
   212  func (s *mockBuilder) SetCmd(c []string) error {
   213  	s.add("CMD " + sliceToString(c))
   214  	return s.err()
   215  }
   216  
   217  func (s *mockBuilder) SetEntrypoint(e []string) error {
   218  	s.add("ENTRYPOINT " + sliceToString(e))
   219  	return s.err()
   220  }
   221  
   222  func (s *mockBuilder) SetStopSignal(sig string) error {
   223  	s.add("STOPSIGNAL " + sig)
   224  	return s.err()
   225  }
   226  
   227  func (s *mockBuilder) SetUser(u string) error {
   228  	s.add("USER " + u)
   229  	return s.err()
   230  }
   231  
   232  func (s *mockBuilder) SetWorkingDir(w string) error {
   233  	s.add("WORKDIR " + w)
   234  	return s.err()
   235  }
   236  
   237  func mapToString(m map[string]string) string {
   238  	l := []string{}
   239  	for k, v := range m {
   240  		l = append(l, strconv.Quote(k)+"="+strconv.Quote(v))
   241  	}
   242  	sort.Strings(l)
   243  	return strings.Join(l, " ")
   244  }
   245  
   246  func sliceToString(l []string) string {
   247  	r := []string{}
   248  	for _, e := range l {
   249  		r = append(r, strconv.Quote(e))
   250  	}
   251  	return strings.Join(r, " ")
   252  }