github.com/u-root/u-root@v7.0.1-0.20200915234505-ad7babab0a8e+incompatible/uroot_test.go (about)

     1  // Copyright 2015-2018 the u-root Authors. All rights reserved
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package main
     6  
     7  import (
     8  	"bufio"
     9  	"bytes"
    10  	"crypto/sha256"
    11  	"fmt"
    12  	"io"
    13  	"io/ioutil"
    14  	"os"
    15  	"os/exec"
    16  	"path/filepath"
    17  	"strings"
    18  	"testing"
    19  
    20  	"github.com/u-root/u-root/pkg/cpio"
    21  	"github.com/u-root/u-root/pkg/golang"
    22  	"github.com/u-root/u-root/pkg/testutil"
    23  	"github.com/u-root/u-root/pkg/uio"
    24  	itest "github.com/u-root/u-root/pkg/uroot/initramfs/test"
    25  )
    26  
    27  var twocmds = []string{
    28  	"github.com/u-root/u-root/cmds/core/ls",
    29  	"github.com/u-root/u-root/cmds/core/init",
    30  }
    31  
    32  var srcmds = []string{
    33  	"github.com/u-root/u-root/cmds/core/ls",
    34  	"github.com/u-root/u-root/cmds/core/init",
    35  	"github.com/u-root/u-root/cmds/core/installcommand",
    36  }
    37  
    38  type buildSourceValidator struct {
    39  	gopath string
    40  	goroot string
    41  	env    []string
    42  }
    43  
    44  func (b buildSourceValidator) Validate(a *cpio.Archive) error {
    45  	dir, err := ioutil.TempDir("", "u-root-source-")
    46  	if err != nil {
    47  		return err
    48  	}
    49  	defer os.RemoveAll(dir)
    50  
    51  	if err := os.Mkdir(filepath.Join(dir, "tmp"), 0755); err != nil {
    52  		return err
    53  	}
    54  
    55  	// Unpack into dir.
    56  	err = cpio.ForEachRecord(a.Reader(), func(r cpio.Record) error {
    57  		return cpio.CreateFileInRoot(r, dir, false)
    58  	})
    59  	if err != nil {
    60  		return err
    61  	}
    62  
    63  	goroot := filepath.Join(dir, b.goroot)
    64  	gopath := filepath.Join(dir, b.gopath)
    65  	// go build ./src/...
    66  	c := exec.Command(filepath.Join(goroot, "bin/go"), "build", filepath.Join(gopath, "src/..."))
    67  	c.Env = append(b.env,
    68  		fmt.Sprintf("GOPATH=%s", gopath),
    69  		fmt.Sprintf("GOCACHE=%s", filepath.Join(dir, "tmp")),
    70  		fmt.Sprintf("GOROOT=%s", goroot),
    71  		"CGO_ENABLED=0")
    72  	out, err := c.CombinedOutput()
    73  	if err != nil {
    74  		return fmt.Errorf("could not build go source %v; output\n%s", err, out)
    75  	}
    76  	return nil
    77  }
    78  
    79  func xTestDCE(t *testing.T) {
    80  	delFiles := false
    81  	f, _ := buildIt(
    82  		t,
    83  		[]string{
    84  			"-build=bb", "-no-strip",
    85  			"./cmds/*/*",
    86  			"-cmds/core/installcommand", "-cmds/exp/builtin", "-cmds/exp/run",
    87  			"pkg/uroot/test/foo",
    88  		},
    89  		nil,
    90  		nil)
    91  	defer func() {
    92  		if delFiles {
    93  			os.RemoveAll(f.Name())
    94  		}
    95  	}()
    96  	st, _ := f.Stat()
    97  	t.Logf("Built %s, size %d", f.Name(), st.Size())
    98  	cmd := golang.Default().GoCmd("tool", "nm", f.Name())
    99  	nmOutput, err := cmd.CombinedOutput()
   100  	if err != nil {
   101  		t.Fatalf("failed to run nm: %s %s", err, nmOutput)
   102  	}
   103  	symScanner := bufio.NewScanner(bytes.NewBuffer(nmOutput))
   104  	syms := map[string]bool{}
   105  	for symScanner.Scan() {
   106  		line := symScanner.Text()
   107  		parts := strings.Split(line, " ")
   108  		if len(parts) == 0 {
   109  			continue
   110  		}
   111  		sym := parts[len(parts)-1]
   112  		syms[sym] = true
   113  		t.Logf("%s", sym)
   114  	}
   115  }
   116  
   117  type noDeadCode struct {
   118  	Path string
   119  }
   120  
   121  func (v noDeadCode) Validate(a *cpio.Archive) error {
   122  	// 1. Extract BB binary into a temporary file.
   123  	delFiles := true
   124  	bbRecord, ok := a.Get(v.Path)
   125  	if !ok {
   126  		return fmt.Errorf("archive does not contain %s, but should", v.Path)
   127  	}
   128  	tf, err := ioutil.TempFile("", "u-root-temp-bb-")
   129  	if err != nil {
   130  		return err
   131  	}
   132  	bbData, _ := uio.ReadAll(bbRecord)
   133  	tf.Write(bbData)
   134  	tf.Close()
   135  	defer func() {
   136  		if delFiles {
   137  			os.RemoveAll(tf.Name())
   138  		}
   139  	}()
   140  	// 2. Run "go nm" on it and build symbol table.
   141  	cmd := golang.Default().GoCmd("tool", "nm", tf.Name())
   142  	nmOutput, err := cmd.CombinedOutput()
   143  	if err != nil {
   144  		return fmt.Errorf("failed to run nm: %s %s", err, nmOutput)
   145  	}
   146  	symScanner := bufio.NewScanner(bytes.NewBuffer(nmOutput))
   147  	syms := map[string]bool{}
   148  	for symScanner.Scan() {
   149  		line := symScanner.Text()
   150  		parts := strings.Split(line, " ")
   151  		if len(parts) == 0 {
   152  			continue
   153  		}
   154  		sym := parts[len(parts)-1]
   155  		syms[sym] = true
   156  	}
   157  	// 3. Check for presence and absence of particular symbols.
   158  	if !syms["github.com/u-root/u-root/pkg/uroot/test/bar.Bar.UsedInterfaceMethod"] {
   159  		// Sanity check of the test itself: this method must be in the binary.
   160  		return fmt.Errorf("expected symbol not found, something is wrong with the build")
   161  	}
   162  	if syms["github.com/u-root/u-root/pkg/uroot/test/bar.Bar.UnusedNonInterfaceMethod"] {
   163  		// Sanity check of the test itself: this method must be in the binary.
   164  		delFiles = false
   165  		return fmt.Errorf(
   166  			"Unused non-interface method has not been eliminated, dead code elimination is not working properly.\n"+
   167  				"The most likely reason is use of reflect.Value.Method or .MethodByName somewhere "+
   168  				"(could be a command or vendor dependency, apologies for not being more precise here).\n"+
   169  				"See https://golang.org/src/cmd/link/internal/ld/deadcode.go for explanation.\n"+
   170  				"%s contains the resulting binary.\n", tf.Name())
   171  	}
   172  	return nil
   173  }
   174  
   175  func TestUrootCmdline(t *testing.T) {
   176  	samplef, err := ioutil.TempFile("", "u-root-test-")
   177  	if err != nil {
   178  		t.Fatal(err)
   179  	}
   180  	samplef.Close()
   181  	defer os.RemoveAll(samplef.Name())
   182  	sampledir, err := ioutil.TempDir("", "u-root-test-dir-")
   183  	if err != nil {
   184  		t.Fatal(err)
   185  	}
   186  	if err = ioutil.WriteFile(filepath.Join(sampledir, "foo"), nil, 0644); err != nil {
   187  		t.Fatal(err)
   188  	}
   189  	if err = ioutil.WriteFile(filepath.Join(sampledir, "bar"), nil, 0644); err != nil {
   190  		t.Fatal(err)
   191  	}
   192  	defer os.RemoveAll(sampledir)
   193  
   194  	for _, tt := range []struct {
   195  		name       string
   196  		env        []string
   197  		args       []string
   198  		err        error
   199  		validators []itest.ArchiveValidator
   200  	}{
   201  		{
   202  			name: "include one extra file",
   203  			args: []string{"-nocmd", "-files=/bin/bash"},
   204  			err:  nil,
   205  			validators: []itest.ArchiveValidator{
   206  				itest.HasFile{"bin/bash"},
   207  			},
   208  		},
   209  		{
   210  			name: "uinitcmd",
   211  			args: []string{"-build=bb", "-uinitcmd=echo foobar fuzz", "-defaultsh=", "./cmds/core/init", "./cmds/core/echo"},
   212  			err:  nil,
   213  			validators: []itest.ArchiveValidator{
   214  				itest.HasRecord{cpio.Symlink("bin/uinit", "../bbin/echo")},
   215  				itest.HasContent{
   216  					Path:    "etc/uinit.flags",
   217  					Content: "\"foobar\"\n\"fuzz\"",
   218  				},
   219  			},
   220  		},
   221  		{
   222  			name: "fix usage of an absolute path",
   223  			args: []string{"-nocmd", fmt.Sprintf("-files=%s:/bin", sampledir)},
   224  			err:  nil,
   225  			validators: []itest.ArchiveValidator{
   226  				itest.HasFile{"/bin/foo"},
   227  				itest.HasFile{"/bin/bar"},
   228  			},
   229  		},
   230  		{
   231  			name: "include multiple extra files",
   232  			args: []string{"-nocmd", "-files=/bin/bash", "-files=/bin/ls", fmt.Sprintf("-files=%s", samplef.Name())},
   233  			validators: []itest.ArchiveValidator{
   234  				itest.HasFile{"bin/bash"},
   235  				itest.HasFile{"bin/ls"},
   236  				itest.HasFile{samplef.Name()},
   237  			},
   238  		},
   239  		{
   240  			name: "include one extra file with rename",
   241  			args: []string{"-nocmd", "-files=/bin/bash:bin/bush"},
   242  			validators: []itest.ArchiveValidator{
   243  				itest.HasFile{"bin/bush"},
   244  			},
   245  		},
   246  		{
   247  			name: "make sure dead code gets eliminated",
   248  			args: []string{
   249  				// Build the world + test symbols, unstripped.
   250  				"-build=bb", "-no-strip", "world", "pkg/uroot/test/foo",
   251  				// These are known to disable DCE and need to be exluded.
   252  				// The reason is https://github.com/golang/go/issues/36021 and is fixed in Go 1.15,
   253  				// so these can be removed once we no longer support Go < 1.15.
   254  				"-cmds/core/installcommand", "-cmds/exp/builtin", "-cmds/exp/run",
   255  			},
   256  			err: nil,
   257  			validators: []itest.ArchiveValidator{
   258  				noDeadCode{Path: "bbin/bb"},
   259  			},
   260  		},
   261  		{
   262  			name: "hosted source mode",
   263  			args: append([]string{"-build=source", "-base=/dev/null", "-defaultsh=", "-initcmd="}, srcmds...),
   264  		},
   265  		{
   266  			name: "hosted bb mode",
   267  			args: append([]string{"-build=bb", "-base=/dev/null", "-defaultsh=", "-initcmd="}, twocmds...),
   268  		},
   269  		{
   270  			name: "AMD64 bb build",
   271  			env:  []string{"GOARCH=amd64"},
   272  			args: []string{"-build=bb", "all"},
   273  		},
   274  		{
   275  			name: "AMD64 source build",
   276  			env:  []string{"GOARCH=amd64"},
   277  			args: []string{"-build=source", "all"},
   278  			validators: []itest.ArchiveValidator{
   279  				buildSourceValidator{
   280  					goroot: "/go",
   281  					gopath: ".",
   282  					env:    []string{"GOARCH=amd64"},
   283  				},
   284  			},
   285  		},
   286  		{
   287  			name: "MIPS bb build",
   288  			env:  []string{"GOARCH=mips"},
   289  			args: []string{"-build=bb", "all"},
   290  		},
   291  		{
   292  			name: "MIPSLE bb build",
   293  			env:  []string{"GOARCH=mipsle"},
   294  			args: []string{"-build=bb", "all"},
   295  		},
   296  		{
   297  			name: "MIPS64 bb build",
   298  			env:  []string{"GOARCH=mips64"},
   299  			args: []string{"-build=bb", "all"},
   300  		},
   301  		{
   302  			name: "MIPS64LE bb build",
   303  			env:  []string{"GOARCH=mips64le"},
   304  			args: []string{"-build=bb", "all"},
   305  		},
   306  		{
   307  			name: "ARM7 bb build",
   308  			env:  []string{"GOARCH=arm", "GOARM=7"},
   309  			args: []string{"-build=bb", "all"},
   310  		},
   311  		{
   312  			name: "ARM64 bb build",
   313  			env:  []string{"GOARCH=arm64"},
   314  			args: []string{"-build=bb", "all"},
   315  		},
   316  		{
   317  			name: "386 (32 bit) bb build",
   318  			env:  []string{"GOARCH=386"},
   319  			args: []string{"-build=bb", "all"},
   320  		},
   321  		{
   322  			name: "Power 64bit bb build",
   323  			env:  []string{"GOARCH=ppc64le"},
   324  			args: []string{"-build=bb", "all"},
   325  		},
   326  	} {
   327  		t.Run(tt.name, func(t *testing.T) {
   328  			delFiles := true
   329  			f, sum1 := buildIt(t, tt.args, tt.env, tt.err)
   330  			defer func() {
   331  				if delFiles {
   332  					os.RemoveAll(f.Name())
   333  				}
   334  			}()
   335  
   336  			a, err := itest.ReadArchive(f.Name())
   337  			if err != nil {
   338  				t.Fatal(err)
   339  			}
   340  
   341  			for _, v := range tt.validators {
   342  				if err := v.Validate(a); err != nil {
   343  					t.Errorf("validator failed: %v / archive:\n%s", err, a)
   344  				}
   345  			}
   346  
   347  			f2, sum2 := buildIt(t, tt.args, tt.env, tt.err)
   348  			defer func() {
   349  				if delFiles {
   350  					os.RemoveAll(f2.Name())
   351  				}
   352  			}()
   353  			if !bytes.Equal(sum1, sum2) {
   354  				delFiles = false
   355  				t.Errorf("not reproducible, hashes don't match")
   356  				t.Errorf("env: %v args: %v", tt.env, tt.args)
   357  				t.Errorf("file1: %v file2: %v", f.Name(), f2.Name())
   358  			}
   359  		})
   360  	}
   361  }
   362  
   363  func buildIt(t *testing.T, args, env []string, want error) (*os.File, []byte) {
   364  	f, err := ioutil.TempFile("", "u-root-")
   365  	if err != nil {
   366  		t.Fatal(err)
   367  	}
   368  
   369  	arg := append([]string{"-o", f.Name()}, args...)
   370  	c := testutil.Command(t, arg...)
   371  	t.Logf("Commandline: %v", arg)
   372  	c.Env = append(c.Env, env...)
   373  	if out, err := c.CombinedOutput(); err != want {
   374  		t.Fatalf("Error: %v\nOutput:\n%s", err, out)
   375  	} else if err != nil {
   376  		h1 := sha256.New()
   377  		if _, err := io.Copy(h1, f); err != nil {
   378  			t.Fatal()
   379  		}
   380  		return f, h1.Sum(nil)
   381  	}
   382  	return f, nil
   383  }
   384  
   385  func TestMain(m *testing.M) {
   386  	testutil.Run(m, main)
   387  }