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