Michael Hudson-Doyle | 362a40e | 2015-05-07 21:29:47 +1200 | [diff] [blame] | 1 | // Copyright 2015 The Go 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 shared_test |
| 6 | |
| 7 | import ( |
| 8 | "bufio" |
| 9 | "bytes" |
| 10 | "debug/elf" |
Michael Hudson-Doyle | 6551803 | 2015-05-25 13:59:08 +1200 | [diff] [blame] | 11 | "encoding/binary" |
Michael Hudson-Doyle | 362a40e | 2015-05-07 21:29:47 +1200 | [diff] [blame] | 12 | "errors" |
| 13 | "flag" |
| 14 | "fmt" |
| 15 | "go/build" |
Michael Hudson-Doyle | 6551803 | 2015-05-25 13:59:08 +1200 | [diff] [blame] | 16 | "io" |
Michael Hudson-Doyle | 362a40e | 2015-05-07 21:29:47 +1200 | [diff] [blame] | 17 | "io/ioutil" |
| 18 | "log" |
| 19 | "math/rand" |
| 20 | "os" |
| 21 | "os/exec" |
| 22 | "path/filepath" |
| 23 | "strings" |
| 24 | "testing" |
| 25 | "time" |
| 26 | ) |
| 27 | |
| 28 | var gopathInstallDir, gorootInstallDir, suffix string |
| 29 | |
| 30 | // This is the smallest set of packages we can link into a shared |
| 31 | // library (runtime/cgo is built implicitly). |
| 32 | var minpkgs = []string{"runtime", "sync/atomic"} |
| 33 | var soname = "libruntime,sync-atomic.so" |
| 34 | |
| 35 | // run runs a command and calls t.Errorf if it fails. |
| 36 | func run(t *testing.T, msg string, args ...string) { |
| 37 | c := exec.Command(args[0], args[1:]...) |
| 38 | if output, err := c.CombinedOutput(); err != nil { |
| 39 | t.Errorf("executing %s (%s) failed %s:\n%s", strings.Join(args, " "), msg, err, output) |
| 40 | } |
| 41 | } |
| 42 | |
| 43 | // goCmd invokes the go tool with the installsuffix set up by TestMain. It calls |
| 44 | // t.Errorf if the command fails. |
| 45 | func goCmd(t *testing.T, args ...string) { |
| 46 | newargs := []string{args[0], "-installsuffix=" + suffix} |
| 47 | if testing.Verbose() { |
| 48 | newargs = append(newargs, "-v") |
| 49 | } |
| 50 | newargs = append(newargs, args[1:]...) |
| 51 | c := exec.Command("go", newargs...) |
Michael Hudson-Doyle | 9262e21 | 2015-05-27 10:30:55 +1200 | [diff] [blame] | 52 | var output []byte |
| 53 | var err error |
Michael Hudson-Doyle | 362a40e | 2015-05-07 21:29:47 +1200 | [diff] [blame] | 54 | if testing.Verbose() { |
| 55 | fmt.Printf("+ go %s\n", strings.Join(newargs, " ")) |
Michael Hudson-Doyle | 9262e21 | 2015-05-27 10:30:55 +1200 | [diff] [blame] | 56 | c.Stdout = os.Stdout |
| 57 | c.Stderr = os.Stderr |
| 58 | err = c.Run() |
| 59 | } else { |
| 60 | output, err = c.CombinedOutput() |
Michael Hudson-Doyle | 362a40e | 2015-05-07 21:29:47 +1200 | [diff] [blame] | 61 | } |
Michael Hudson-Doyle | 9262e21 | 2015-05-27 10:30:55 +1200 | [diff] [blame] | 62 | if err != nil { |
Michael Hudson-Doyle | 362a40e | 2015-05-07 21:29:47 +1200 | [diff] [blame] | 63 | if t != nil { |
| 64 | t.Errorf("executing %s failed %v:\n%s", strings.Join(c.Args, " "), err, output) |
| 65 | } else { |
| 66 | log.Fatalf("executing %s failed %v:\n%s", strings.Join(c.Args, " "), err, output) |
| 67 | } |
| 68 | } |
| 69 | } |
| 70 | |
| 71 | // TestMain calls testMain so that the latter can use defer (TestMain exits with os.Exit). |
| 72 | func testMain(m *testing.M) (int, error) { |
| 73 | // Because go install -buildmode=shared $standard_library_package always |
| 74 | // installs into $GOROOT, here are some gymnastics to come up with a unique |
| 75 | // installsuffix to use in this test that we can clean up afterwards. |
| 76 | myContext := build.Default |
| 77 | runtimeP, err := myContext.Import("runtime", ".", build.ImportComment) |
| 78 | if err != nil { |
| 79 | return 0, fmt.Errorf("import failed: %v", err) |
| 80 | } |
| 81 | for i := 0; i < 10000; i++ { |
| 82 | try := fmt.Sprintf("%s_%d_dynlink", runtimeP.PkgTargetRoot, rand.Int63()) |
| 83 | err = os.Mkdir(try, 0700) |
| 84 | if os.IsExist(err) { |
| 85 | continue |
| 86 | } |
| 87 | if err == nil { |
| 88 | gorootInstallDir = try |
| 89 | } |
| 90 | break |
| 91 | } |
| 92 | if err != nil { |
| 93 | return 0, fmt.Errorf("can't create temporary directory: %v", err) |
| 94 | } |
| 95 | if gorootInstallDir == "" { |
| 96 | return 0, errors.New("could not create temporary directory after 10000 tries") |
| 97 | } |
| 98 | defer os.RemoveAll(gorootInstallDir) |
| 99 | |
| 100 | // Some tests need to edit the source in GOPATH, so copy this directory to a |
| 101 | // temporary directory and chdir to that. |
| 102 | scratchDir, err := ioutil.TempDir("", "testshared") |
| 103 | if err != nil { |
| 104 | return 0, fmt.Errorf("TempDir failed: %v", err) |
| 105 | } |
| 106 | defer os.RemoveAll(scratchDir) |
| 107 | err = filepath.Walk(".", func(path string, info os.FileInfo, err error) error { |
| 108 | scratchPath := filepath.Join(scratchDir, path) |
| 109 | if info.IsDir() { |
| 110 | if path == "." { |
| 111 | return nil |
| 112 | } |
| 113 | return os.Mkdir(scratchPath, info.Mode()) |
| 114 | } else { |
| 115 | fromBytes, err := ioutil.ReadFile(path) |
| 116 | if err != nil { |
| 117 | return err |
| 118 | } |
| 119 | return ioutil.WriteFile(scratchPath, fromBytes, info.Mode()) |
| 120 | } |
| 121 | }) |
| 122 | if err != nil { |
| 123 | return 0, fmt.Errorf("walk failed: %v", err) |
| 124 | } |
| 125 | os.Setenv("GOPATH", scratchDir) |
| 126 | myContext.GOPATH = scratchDir |
| 127 | os.Chdir(scratchDir) |
| 128 | |
| 129 | // All tests depend on runtime being built into a shared library. Because |
| 130 | // that takes a few seconds, do it here and have all tests use the version |
| 131 | // built here. |
| 132 | suffix = strings.Split(filepath.Base(gorootInstallDir), "_")[2] |
| 133 | goCmd(nil, append([]string{"install", "-buildmode=shared"}, minpkgs...)...) |
| 134 | |
| 135 | myContext.InstallSuffix = suffix + "_dynlink" |
| 136 | depP, err := myContext.Import("dep", ".", build.ImportComment) |
| 137 | if err != nil { |
| 138 | return 0, fmt.Errorf("import failed: %v", err) |
| 139 | } |
| 140 | gopathInstallDir = depP.PkgTargetRoot |
| 141 | return m.Run(), nil |
| 142 | } |
| 143 | |
| 144 | func TestMain(m *testing.M) { |
| 145 | flag.Parse() |
| 146 | exitCode, err := testMain(m) |
| 147 | if err != nil { |
| 148 | log.Fatal(err) |
| 149 | } |
| 150 | os.Exit(exitCode) |
| 151 | } |
| 152 | |
| 153 | // The shared library was built at the expected location. |
| 154 | func TestSOBuilt(t *testing.T) { |
| 155 | _, err := os.Stat(filepath.Join(gorootInstallDir, soname)) |
| 156 | if err != nil { |
| 157 | t.Error(err) |
| 158 | } |
| 159 | } |
| 160 | |
| 161 | // The install command should have created a "shlibname" file for the |
| 162 | // listed packages (and runtime/cgo) indicating the name of the shared |
| 163 | // library containing it. |
| 164 | func TestShlibnameFiles(t *testing.T) { |
| 165 | pkgs := append([]string{}, minpkgs...) |
| 166 | pkgs = append(pkgs, "runtime/cgo") |
| 167 | for _, pkg := range pkgs { |
| 168 | shlibnamefile := filepath.Join(gorootInstallDir, pkg+".shlibname") |
| 169 | contentsb, err := ioutil.ReadFile(shlibnamefile) |
| 170 | if err != nil { |
| 171 | t.Errorf("error reading shlibnamefile for %s: %v", pkg, err) |
| 172 | continue |
| 173 | } |
| 174 | contents := strings.TrimSpace(string(contentsb)) |
| 175 | if contents != soname { |
| 176 | t.Errorf("shlibnamefile for %s has wrong contents: %q", pkg, contents) |
| 177 | } |
| 178 | } |
| 179 | } |
| 180 | |
Michael Hudson-Doyle | 6551803 | 2015-05-25 13:59:08 +1200 | [diff] [blame] | 181 | // Is a given offset into the file contained in a loaded segment? |
| 182 | func isOffsetLoaded(f *elf.File, offset uint64) bool { |
| 183 | for _, prog := range f.Progs { |
| 184 | if prog.Type == elf.PT_LOAD { |
| 185 | if prog.Off <= offset && offset < prog.Off+prog.Filesz { |
| 186 | return true |
| 187 | } |
| 188 | } |
| 189 | } |
| 190 | return false |
| 191 | } |
| 192 | |
| 193 | func rnd(v int32, r int32) int32 { |
| 194 | if r <= 0 { |
| 195 | return v |
| 196 | } |
| 197 | v += r - 1 |
| 198 | c := v % r |
| 199 | if c < 0 { |
| 200 | c += r |
| 201 | } |
| 202 | v -= c |
| 203 | return v |
| 204 | } |
| 205 | |
| 206 | func readwithpad(r io.Reader, sz int32) ([]byte, error) { |
| 207 | data := make([]byte, rnd(sz, 4)) |
| 208 | _, err := io.ReadFull(r, data) |
| 209 | if err != nil { |
| 210 | return nil, err |
| 211 | } |
| 212 | data = data[:sz] |
| 213 | return data, nil |
| 214 | } |
| 215 | |
| 216 | type note struct { |
| 217 | name string |
| 218 | tag int32 |
| 219 | desc string |
| 220 | section *elf.Section |
| 221 | } |
| 222 | |
| 223 | // Read all notes from f. As ELF section names are not supposed to be special, one |
| 224 | // looks for a particular note by scanning all SHT_NOTE sections looking for a note |
| 225 | // with a particular "name" and "tag". |
| 226 | func readNotes(f *elf.File) ([]*note, error) { |
| 227 | var notes []*note |
| 228 | for _, sect := range f.Sections { |
| 229 | if sect.Type != elf.SHT_NOTE { |
| 230 | continue |
| 231 | } |
| 232 | r := sect.Open() |
| 233 | for { |
| 234 | var namesize, descsize, tag int32 |
| 235 | err := binary.Read(r, f.ByteOrder, &namesize) |
| 236 | if err != nil { |
| 237 | if err == io.EOF { |
| 238 | break |
| 239 | } |
| 240 | return nil, fmt.Errorf("read namesize failed:", err) |
| 241 | } |
| 242 | err = binary.Read(r, f.ByteOrder, &descsize) |
| 243 | if err != nil { |
| 244 | return nil, fmt.Errorf("read descsize failed:", err) |
| 245 | } |
| 246 | err = binary.Read(r, f.ByteOrder, &tag) |
| 247 | if err != nil { |
| 248 | return nil, fmt.Errorf("read type failed:", err) |
| 249 | } |
| 250 | name, err := readwithpad(r, namesize) |
| 251 | if err != nil { |
| 252 | return nil, fmt.Errorf("read name failed:", err) |
| 253 | } |
| 254 | desc, err := readwithpad(r, descsize) |
| 255 | if err != nil { |
| 256 | return nil, fmt.Errorf("read desc failed:", err) |
| 257 | } |
| 258 | notes = append(notes, ¬e{name: string(name), tag: tag, desc: string(desc), section: sect}) |
| 259 | } |
| 260 | } |
| 261 | return notes, nil |
| 262 | } |
| 263 | |
Michael Hudson-Doyle | 362a40e | 2015-05-07 21:29:47 +1200 | [diff] [blame] | 264 | func dynStrings(path string, flag elf.DynTag) []string { |
| 265 | f, err := elf.Open(path) |
| 266 | defer f.Close() |
| 267 | if err != nil { |
| 268 | log.Fatal("elf.Open failed: ", err) |
| 269 | } |
| 270 | dynstrings, err := f.DynString(flag) |
| 271 | if err != nil { |
| 272 | log.Fatal("dynstring failed: ", err) |
| 273 | } |
| 274 | return dynstrings |
| 275 | } |
| 276 | |
| 277 | func AssertIsLinkedTo(t *testing.T, path, lib string) { |
| 278 | for _, dynstring := range dynStrings(path, elf.DT_NEEDED) { |
| 279 | if dynstring == lib { |
| 280 | return |
| 281 | } |
| 282 | } |
| 283 | t.Errorf("%s is not linked to %s", path, lib) |
| 284 | } |
| 285 | |
| 286 | func AssertHasRPath(t *testing.T, path, dir string) { |
Shenghou Ma | c013417 | 2015-05-19 02:48:15 -0400 | [diff] [blame] | 287 | for _, tag := range []elf.DynTag{elf.DT_RPATH, elf.DT_RUNPATH} { |
| 288 | for _, dynstring := range dynStrings(path, tag) { |
| 289 | for _, rpath := range strings.Split(dynstring, ":") { |
| 290 | if filepath.Clean(rpath) == filepath.Clean(dir) { |
| 291 | return |
| 292 | } |
Michael Hudson-Doyle | 362a40e | 2015-05-07 21:29:47 +1200 | [diff] [blame] | 293 | } |
| 294 | } |
| 295 | } |
| 296 | t.Errorf("%s does not have rpath %s", path, dir) |
| 297 | } |
| 298 | |
| 299 | // Build a trivial program that links against the shared runtime and check it runs. |
| 300 | func TestTrivialExecutable(t *testing.T) { |
| 301 | goCmd(t, "install", "-linkshared", "trivial") |
| 302 | run(t, "trivial executable", "./bin/trivial") |
| 303 | AssertIsLinkedTo(t, "./bin/trivial", soname) |
| 304 | AssertHasRPath(t, "./bin/trivial", gorootInstallDir) |
| 305 | } |
| 306 | |
| 307 | // Build a GOPATH package into a shared library that links against the goroot runtime |
| 308 | // and an executable that links against both. |
| 309 | func TestGOPathShlib(t *testing.T) { |
| 310 | goCmd(t, "install", "-buildmode=shared", "-linkshared", "dep") |
| 311 | AssertIsLinkedTo(t, filepath.Join(gopathInstallDir, "libdep.so"), soname) |
| 312 | goCmd(t, "install", "-linkshared", "exe") |
| 313 | AssertIsLinkedTo(t, "./bin/exe", soname) |
| 314 | AssertIsLinkedTo(t, "./bin/exe", "libdep.so") |
| 315 | AssertHasRPath(t, "./bin/exe", gorootInstallDir) |
| 316 | AssertHasRPath(t, "./bin/exe", gopathInstallDir) |
| 317 | // And check it runs. |
| 318 | run(t, "executable linked to GOPATH library", "./bin/exe") |
| 319 | } |
| 320 | |
Michael Hudson-Doyle | 6551803 | 2015-05-25 13:59:08 +1200 | [diff] [blame] | 321 | // The shared library contains a note listing the packages it contains in a section |
| 322 | // that is not mapped into memory. |
| 323 | func testPkgListNote(t *testing.T, f *elf.File, note *note) { |
| 324 | if note.section.Flags != 0 { |
| 325 | t.Errorf("package list section has flags %v", note.section.Flags) |
| 326 | } |
| 327 | if isOffsetLoaded(f, note.section.Offset) { |
| 328 | t.Errorf("package list section contained in PT_LOAD segment") |
| 329 | } |
| 330 | if note.desc != "dep\n" { |
| 331 | t.Errorf("incorrect package list %q", note.desc) |
| 332 | } |
| 333 | } |
| 334 | |
| 335 | // The shared library contains a note containing the ABI hash that is mapped into |
| 336 | // memory and there is a local symbol called go.link.abihashbytes that points 16 |
| 337 | // bytes into it. |
| 338 | func testABIHashNote(t *testing.T, f *elf.File, note *note) { |
| 339 | if note.section.Flags != elf.SHF_ALLOC { |
| 340 | t.Errorf("abi hash section has flags %v", note.section.Flags) |
| 341 | } |
| 342 | if !isOffsetLoaded(f, note.section.Offset) { |
| 343 | t.Errorf("abihash section not contained in PT_LOAD segment") |
| 344 | } |
| 345 | var hashbytes elf.Symbol |
| 346 | symbols, err := f.Symbols() |
| 347 | if err != nil { |
| 348 | t.Errorf("error reading symbols %v", err) |
| 349 | return |
| 350 | } |
| 351 | for _, sym := range symbols { |
| 352 | if sym.Name == "go.link.abihashbytes" { |
| 353 | hashbytes = sym |
| 354 | } |
| 355 | } |
| 356 | if hashbytes.Name == "" { |
| 357 | t.Errorf("no symbol called go.link.abihashbytes") |
| 358 | return |
| 359 | } |
| 360 | if elf.ST_BIND(hashbytes.Info) != elf.STB_LOCAL { |
| 361 | t.Errorf("%s has incorrect binding %v", hashbytes.Name, elf.ST_BIND(hashbytes.Info)) |
| 362 | } |
| 363 | if f.Sections[hashbytes.Section] != note.section { |
| 364 | t.Errorf("%s has incorrect section %v", hashbytes.Name, f.Sections[hashbytes.Section].Name) |
| 365 | } |
| 366 | if hashbytes.Value-note.section.Addr != 16 { |
| 367 | t.Errorf("%s has incorrect offset into section %d", hashbytes.Name, hashbytes.Value-note.section.Addr) |
| 368 | } |
| 369 | } |
| 370 | |
Michael Hudson-Doyle | bcc1870 | 2015-05-25 14:51:02 +1200 | [diff] [blame] | 371 | // A Go shared library contains a note indicating which other Go shared libraries it |
| 372 | // was linked against in an unmapped section. |
| 373 | func testDepsNote(t *testing.T, f *elf.File, note *note) { |
| 374 | if note.section.Flags != 0 { |
| 375 | t.Errorf("package list section has flags %v", note.section.Flags) |
| 376 | } |
| 377 | if isOffsetLoaded(f, note.section.Offset) { |
| 378 | t.Errorf("package list section contained in PT_LOAD segment") |
| 379 | } |
| 380 | // libdep.so just links against the lib containing the runtime. |
| 381 | if note.desc != soname { |
| 382 | t.Errorf("incorrect dependency list %q", note.desc) |
| 383 | } |
| 384 | } |
| 385 | |
Michael Hudson-Doyle | 6551803 | 2015-05-25 13:59:08 +1200 | [diff] [blame] | 386 | // The shared library contains notes with defined contents; see above. |
| 387 | func TestNotes(t *testing.T) { |
| 388 | goCmd(t, "install", "-buildmode=shared", "-linkshared", "dep") |
| 389 | f, err := elf.Open(filepath.Join(gopathInstallDir, "libdep.so")) |
| 390 | if err != nil { |
| 391 | t.Fatal(err) |
| 392 | } |
| 393 | defer f.Close() |
| 394 | notes, err := readNotes(f) |
| 395 | if err != nil { |
| 396 | t.Fatal(err) |
| 397 | } |
| 398 | pkgListNoteFound := false |
| 399 | abiHashNoteFound := false |
Michael Hudson-Doyle | bcc1870 | 2015-05-25 14:51:02 +1200 | [diff] [blame] | 400 | depsNoteFound := false |
Michael Hudson-Doyle | 6551803 | 2015-05-25 13:59:08 +1200 | [diff] [blame] | 401 | for _, note := range notes { |
Russ Cox | 7e27625 | 2015-06-04 14:27:39 -0400 | [diff] [blame^] | 402 | if note.name != "Go\x00\x00" { |
Michael Hudson-Doyle | 6551803 | 2015-05-25 13:59:08 +1200 | [diff] [blame] | 403 | continue |
| 404 | } |
| 405 | switch note.tag { |
| 406 | case 1: // ELF_NOTE_GOPKGLIST_TAG |
| 407 | if pkgListNoteFound { |
| 408 | t.Error("multiple package list notes") |
| 409 | } |
| 410 | testPkgListNote(t, f, note) |
| 411 | pkgListNoteFound = true |
| 412 | case 2: // ELF_NOTE_GOABIHASH_TAG |
| 413 | if abiHashNoteFound { |
| 414 | t.Error("multiple abi hash notes") |
| 415 | } |
| 416 | testABIHashNote(t, f, note) |
| 417 | abiHashNoteFound = true |
Michael Hudson-Doyle | bcc1870 | 2015-05-25 14:51:02 +1200 | [diff] [blame] | 418 | case 3: // ELF_NOTE_GODEPS_TAG |
| 419 | if depsNoteFound { |
| 420 | t.Error("multiple abi hash notes") |
| 421 | } |
| 422 | testDepsNote(t, f, note) |
| 423 | depsNoteFound = true |
Michael Hudson-Doyle | 6551803 | 2015-05-25 13:59:08 +1200 | [diff] [blame] | 424 | } |
| 425 | } |
| 426 | if !pkgListNoteFound { |
| 427 | t.Error("package list note not found") |
| 428 | } |
| 429 | if !abiHashNoteFound { |
| 430 | t.Error("abi hash note not found") |
| 431 | } |
Michael Hudson-Doyle | bcc1870 | 2015-05-25 14:51:02 +1200 | [diff] [blame] | 432 | if !depsNoteFound { |
| 433 | t.Error("deps note not found") |
| 434 | } |
| 435 | } |
| 436 | |
| 437 | // Build a GOPATH package (dep) into a shared library that links against the goroot |
| 438 | // runtime, another package (dep2) that links against the first, and and an |
| 439 | // executable that links against dep2. |
| 440 | func TestTwoGOPathShlibs(t *testing.T) { |
| 441 | goCmd(t, "install", "-buildmode=shared", "-linkshared", "dep") |
| 442 | goCmd(t, "install", "-buildmode=shared", "-linkshared", "dep2") |
| 443 | goCmd(t, "install", "-linkshared", "exe2") |
| 444 | run(t, "executable linked to GOPATH library", "./bin/exe2") |
Michael Hudson-Doyle | 6551803 | 2015-05-25 13:59:08 +1200 | [diff] [blame] | 445 | } |
| 446 | |
Michael Hudson-Doyle | 362a40e | 2015-05-07 21:29:47 +1200 | [diff] [blame] | 447 | // Testing rebuilding of shared libraries when they are stale is a bit more |
| 448 | // complicated that it seems like it should be. First, we make everything "old": but |
| 449 | // only a few seconds old, or it might be older than 6g (or the runtime source) and |
| 450 | // everything will get rebuilt. Then define a timestamp slightly newer than this |
| 451 | // time, which is what we set the mtime to of a file to cause it to be seen as new, |
| 452 | // and finally another slightly even newer one that we can compare files against to |
| 453 | // see if they have been rebuilt. |
| 454 | var oldTime = time.Now().Add(-9 * time.Second) |
| 455 | var nearlyNew = time.Now().Add(-6 * time.Second) |
| 456 | var stampTime = time.Now().Add(-3 * time.Second) |
| 457 | |
| 458 | // resetFileStamps makes "everything" (bin, src, pkg from GOPATH and the |
| 459 | // test-specific parts of GOROOT) appear old. |
| 460 | func resetFileStamps() { |
| 461 | chtime := func(path string, info os.FileInfo, err error) error { |
| 462 | return os.Chtimes(path, oldTime, oldTime) |
| 463 | } |
| 464 | reset := func(path string) { |
| 465 | if err := filepath.Walk(path, chtime); err != nil { |
| 466 | log.Fatalf("resetFileStamps failed: %v", err) |
| 467 | } |
| 468 | |
| 469 | } |
| 470 | reset("bin") |
| 471 | reset("pkg") |
| 472 | reset("src") |
| 473 | reset(gorootInstallDir) |
| 474 | } |
| 475 | |
| 476 | // touch makes path newer than the "old" time stamp used by resetFileStamps. |
| 477 | func touch(path string) { |
| 478 | if err := os.Chtimes(path, nearlyNew, nearlyNew); err != nil { |
| 479 | log.Fatalf("os.Chtimes failed: %v", err) |
| 480 | } |
| 481 | } |
| 482 | |
| 483 | // isNew returns if the path is newer than the time stamp used by touch. |
| 484 | func isNew(path string) bool { |
| 485 | fi, err := os.Stat(path) |
| 486 | if err != nil { |
| 487 | log.Fatalf("os.Stat failed: %v", err) |
| 488 | } |
| 489 | return fi.ModTime().After(stampTime) |
| 490 | } |
| 491 | |
| 492 | // Fail unless path has been rebuilt (i.e. is newer than the time stamp used by |
| 493 | // isNew) |
| 494 | func AssertRebuilt(t *testing.T, msg, path string) { |
| 495 | if !isNew(path) { |
| 496 | t.Errorf("%s was not rebuilt (%s)", msg, path) |
| 497 | } |
| 498 | } |
| 499 | |
| 500 | // Fail if path has been rebuilt (i.e. is newer than the time stamp used by isNew) |
| 501 | func AssertNotRebuilt(t *testing.T, msg, path string) { |
| 502 | if isNew(path) { |
| 503 | t.Errorf("%s was rebuilt (%s)", msg, path) |
| 504 | } |
| 505 | } |
| 506 | |
| 507 | func TestRebuilding(t *testing.T) { |
| 508 | goCmd(t, "install", "-buildmode=shared", "-linkshared", "dep") |
| 509 | goCmd(t, "install", "-linkshared", "exe") |
| 510 | |
| 511 | // If the source is newer than both the .a file and the .so, both are rebuilt. |
| 512 | resetFileStamps() |
| 513 | touch("src/dep/dep.go") |
| 514 | goCmd(t, "install", "-linkshared", "exe") |
| 515 | AssertRebuilt(t, "new source", filepath.Join(gopathInstallDir, "dep.a")) |
| 516 | AssertRebuilt(t, "new source", filepath.Join(gopathInstallDir, "libdep.so")) |
| 517 | |
| 518 | // If the .a file is newer than the .so, the .so is rebuilt (but not the .a) |
| 519 | resetFileStamps() |
| 520 | touch(filepath.Join(gopathInstallDir, "dep.a")) |
| 521 | goCmd(t, "install", "-linkshared", "exe") |
| 522 | AssertNotRebuilt(t, "new .a file", filepath.Join(gopathInstallDir, "dep.a")) |
| 523 | AssertRebuilt(t, "new .a file", filepath.Join(gopathInstallDir, "libdep.so")) |
| 524 | } |
| 525 | |
| 526 | func appendFile(path, content string) { |
| 527 | f, err := os.OpenFile(path, os.O_WRONLY|os.O_APPEND, 0660) |
| 528 | if err != nil { |
| 529 | log.Fatalf("os.OpenFile failed: %v", err) |
| 530 | } |
| 531 | defer func() { |
| 532 | err := f.Close() |
| 533 | if err != nil { |
| 534 | log.Fatalf("f.Close failed: %v", err) |
| 535 | } |
| 536 | }() |
| 537 | _, err = f.WriteString(content) |
| 538 | if err != nil { |
| 539 | log.Fatalf("f.WriteString failed: %v", err) |
| 540 | } |
| 541 | } |
| 542 | |
| 543 | func TestABIChecking(t *testing.T) { |
| 544 | goCmd(t, "install", "-buildmode=shared", "-linkshared", "dep") |
| 545 | goCmd(t, "install", "-linkshared", "exe") |
| 546 | |
| 547 | // If we make an ABI-breaking change to dep and rebuild libp.so but not exe, |
| 548 | // exe will abort with a complaint on startup. |
| 549 | // This assumes adding an exported function breaks ABI, which is not true in |
| 550 | // some senses but suffices for the narrow definition of ABI compatiblity the |
| 551 | // toolchain uses today. |
| 552 | appendFile("src/dep/dep.go", "func ABIBreak() {}\n") |
| 553 | goCmd(t, "install", "-buildmode=shared", "-linkshared", "dep") |
| 554 | c := exec.Command("./bin/exe") |
| 555 | output, err := c.CombinedOutput() |
| 556 | if err == nil { |
| 557 | t.Fatal("executing exe did not fail after ABI break") |
| 558 | } |
| 559 | scanner := bufio.NewScanner(bytes.NewReader(output)) |
| 560 | foundMsg := false |
| 561 | const wantLine = "abi mismatch detected between the executable and libdep.so" |
| 562 | for scanner.Scan() { |
| 563 | if scanner.Text() == wantLine { |
| 564 | foundMsg = true |
| 565 | break |
| 566 | } |
| 567 | } |
| 568 | if err = scanner.Err(); err != nil { |
| 569 | t.Errorf("scanner encountered error: %v", err) |
| 570 | } |
| 571 | if !foundMsg { |
| 572 | t.Fatalf("exe failed, but without line %q; got output:\n%s", wantLine, output) |
| 573 | } |
| 574 | |
| 575 | // Rebuilding exe makes it work again. |
| 576 | goCmd(t, "install", "-linkshared", "exe") |
| 577 | run(t, "rebuilt exe", "./bin/exe") |
| 578 | |
| 579 | // If we make a change which does not break ABI (such as adding an unexported |
| 580 | // function) and rebuild libdep.so, exe still works. |
| 581 | appendFile("src/dep/dep.go", "func noABIBreak() {}\n") |
| 582 | goCmd(t, "install", "-buildmode=shared", "-linkshared", "dep") |
| 583 | run(t, "after non-ABI breaking change", "./bin/exe") |
| 584 | } |