// Copyright 2016 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package carchive_test

import (
	"bufio"
	"bytes"
	"debug/elf"
	"flag"
	"fmt"
	"io"
	"log"
	"os"
	"os/exec"
	"path/filepath"
	"regexp"
	"runtime"
	"strconv"
	"strings"
	"sync"
	"syscall"
	"testing"
	"time"
	"unicode"
)

// Program to run.
var bin []string

// C compiler with args (from $(go env CC) $(go env GOGCCFLAGS)).
var cc []string

// ".exe" on Windows.
var exeSuffix string

var GOOS, GOARCH, GOPATH string
var libgodir string

var testWork bool // If true, preserve temporary directories.

func TestMain(m *testing.M) {
	flag.BoolVar(&testWork, "testwork", false, "if true, log and preserve the test's temporary working directory")
	flag.Parse()
	if testing.Short() && os.Getenv("GO_BUILDER_NAME") == "" {
		fmt.Printf("SKIP - short mode and $GO_BUILDER_NAME not set\n")
		os.Exit(0)
	}
	if runtime.GOOS == "linux" {
		if _, err := os.Stat("/etc/alpine-release"); err == nil {
			fmt.Printf("SKIP - skipping failing test on alpine - go.dev/issue/19938\n")
			os.Exit(0)
		}
	}

	log.SetFlags(log.Lshortfile)
	os.Exit(testMain(m))
}

func testMain(m *testing.M) int {
	// We need a writable GOPATH in which to run the tests.
	// Construct one in a temporary directory.
	var err error
	GOPATH, err = os.MkdirTemp("", "carchive_test")
	if err != nil {
		log.Panic(err)
	}
	if testWork {
		log.Println(GOPATH)
	} else {
		defer os.RemoveAll(GOPATH)
	}
	os.Setenv("GOPATH", GOPATH)

	// Copy testdata into GOPATH/src/testarchive, along with a go.mod file
	// declaring the same path.
	modRoot := filepath.Join(GOPATH, "src", "testcarchive")
	if err := overlayDir(modRoot, "testdata"); err != nil {
		log.Panic(err)
	}
	if err := os.Chdir(modRoot); err != nil {
		log.Panic(err)
	}
	os.Setenv("PWD", modRoot)
	if err := os.WriteFile("go.mod", []byte("module testcarchive\n"), 0666); err != nil {
		log.Panic(err)
	}

	GOOS = goEnv("GOOS")
	GOARCH = goEnv("GOARCH")
	bin = cmdToRun("./testp")

	ccOut := goEnv("CC")
	cc = []string{string(ccOut)}

	out := goEnv("GOGCCFLAGS")
	quote := '\000'
	start := 0
	lastSpace := true
	backslash := false
	s := string(out)
	for i, c := range s {
		if quote == '\000' && unicode.IsSpace(c) {
			if !lastSpace {
				cc = append(cc, s[start:i])
				lastSpace = true
			}
		} else {
			if lastSpace {
				start = i
				lastSpace = false
			}
			if quote == '\000' && !backslash && (c == '"' || c == '\'') {
				quote = c
				backslash = false
			} else if !backslash && quote == c {
				quote = '\000'
			} else if (quote == '\000' || quote == '"') && !backslash && c == '\\' {
				backslash = true
			} else {
				backslash = false
			}
		}
	}
	if !lastSpace {
		cc = append(cc, s[start:])
	}

	if GOOS == "aix" {
		// -Wl,-bnoobjreorder is mandatory to keep the same layout
		// in .text section.
		cc = append(cc, "-Wl,-bnoobjreorder")
	}
	libbase := GOOS + "_" + GOARCH
	if runtime.Compiler == "gccgo" {
		libbase = "gccgo_" + libgodir + "_fPIC"
	} else {
		switch GOOS {
		case "darwin", "ios":
			if GOARCH == "arm64" {
				libbase += "_shared"
			}
		case "dragonfly", "freebsd", "linux", "netbsd", "openbsd", "solaris", "illumos":
			libbase += "_shared"
		}
	}
	libgodir = filepath.Join(GOPATH, "pkg", libbase, "testcarchive")
	cc = append(cc, "-I", libgodir)

	// Force reallocation (and avoid aliasing bugs) for parallel tests that append to cc.
	cc = cc[:len(cc):len(cc)]

	if GOOS == "windows" {
		exeSuffix = ".exe"
	}

	return m.Run()
}

func goEnv(key string) string {
	out, err := exec.Command("go", "env", key).Output()
	if err != nil {
		if ee, ok := err.(*exec.ExitError); ok {
			fmt.Fprintf(os.Stderr, "%s", ee.Stderr)
		}
		log.Panicf("go env %s failed:\n%s\n", key, err)
	}
	return strings.TrimSpace(string(out))
}

func cmdToRun(name string) []string {
	execScript := "go_" + goEnv("GOOS") + "_" + goEnv("GOARCH") + "_exec"
	executor, err := exec.LookPath(execScript)
	if err != nil {
		return []string{name}
	}
	return []string{executor, name}
}

// genHeader writes a C header file for the C-exported declarations found in .go
// source files in dir.
//
// TODO(golang.org/issue/35715): This should be simpler.
func genHeader(t *testing.T, header, dir string) {
	t.Helper()

	// The 'cgo' command generates a number of additional artifacts,
	// but we're only interested in the header.
	// Shunt the rest of the outputs to a temporary directory.
	objDir, err := os.MkdirTemp(GOPATH, "_obj")
	if err != nil {
		t.Fatal(err)
	}
	defer os.RemoveAll(objDir)

	files, err := filepath.Glob(filepath.Join(dir, "*.go"))
	if err != nil {
		t.Fatal(err)
	}

	cmd := exec.Command("go", "tool", "cgo",
		"-objdir", objDir,
		"-exportheader", header)
	cmd.Args = append(cmd.Args, files...)
	t.Log(cmd.Args)
	if out, err := cmd.CombinedOutput(); err != nil {
		t.Logf("%s", out)
		t.Fatal(err)
	}
}

func testInstall(t *testing.T, exe, libgoa, libgoh string, buildcmd ...string) {
	t.Helper()
	cmd := exec.Command(buildcmd[0], buildcmd[1:]...)
	cmd.Env = append(cmd.Environ(), "GO111MODULE=off") // 'go install' only works in GOPATH mode
	t.Log(buildcmd)
	if out, err := cmd.CombinedOutput(); err != nil {
		t.Logf("%s", out)
		t.Fatal(err)
	}
	if !testWork {
		defer func() {
			os.Remove(libgoa)
			os.Remove(libgoh)
		}()
	}

	ccArgs := append(cc, "-o", exe, "main.c")
	if GOOS == "windows" {
		ccArgs = append(ccArgs, "main_windows.c", libgoa, "-lntdll", "-lws2_32", "-lwinmm")
	} else {
		ccArgs = append(ccArgs, "main_unix.c", libgoa)
	}
	if runtime.Compiler == "gccgo" {
		ccArgs = append(ccArgs, "-lgo")
	}
	t.Log(ccArgs)
	if out, err := exec.Command(ccArgs[0], ccArgs[1:]...).CombinedOutput(); err != nil {
		t.Logf("%s", out)
		t.Fatal(err)
	}
	if !testWork {
		defer os.Remove(exe)
	}

	binArgs := append(cmdToRun(exe), "arg1", "arg2")
	cmd = exec.Command(binArgs[0], binArgs[1:]...)
	if runtime.Compiler == "gccgo" {
		cmd.Env = append(cmd.Environ(), "GCCGO=1")
	}
	if out, err := cmd.CombinedOutput(); err != nil {
		t.Logf("%s", out)
		t.Fatal(err)
	}

	checkLineComments(t, libgoh)
}

var badLineRegexp = regexp.MustCompile(`(?m)^#line [0-9]+ "/.*$`)

// checkLineComments checks that the export header generated by
// -buildmode=c-archive doesn't have any absolute paths in the #line
// comments. We don't want those paths because they are unhelpful for
// the user and make the files change based on details of the location
// of GOPATH.
func checkLineComments(t *testing.T, hdrname string) {
	hdr, err := os.ReadFile(hdrname)
	if err != nil {
		if !os.IsNotExist(err) {
			t.Error(err)
		}
		return
	}
	if line := badLineRegexp.Find(hdr); line != nil {
		t.Errorf("bad #line directive with absolute path in %s: %q", hdrname, line)
	}
}

// checkArchive verifies that the created library looks OK.
// We just check a couple of things now, we can add more checks as needed.
func checkArchive(t *testing.T, arname string) {
	t.Helper()

	switch GOOS {
	case "aix", "darwin", "ios", "windows":
		// We don't have any checks for non-ELF libraries yet.
		if _, err := os.Stat(arname); err != nil {
			t.Errorf("archive %s does not exist: %v", arname, err)
		}
	default:
		checkELFArchive(t, arname)
	}
}

// checkELFArchive checks an ELF archive.
func checkELFArchive(t *testing.T, arname string) {
	t.Helper()

	f, err := os.Open(arname)
	if err != nil {
		t.Errorf("archive %s does not exist: %v", arname, err)
		return
	}
	defer f.Close()

	// TODO(iant): put these in a shared package?  But where?
	const (
		magic = "!<arch>\n"
		fmag  = "`\n"

		namelen = 16
		datelen = 12
		uidlen  = 6
		gidlen  = 6
		modelen = 8
		sizelen = 10
		fmaglen = 2
		hdrlen  = namelen + datelen + uidlen + gidlen + modelen + sizelen + fmaglen
	)

	type arhdr struct {
		name string
		date string
		uid  string
		gid  string
		mode string
		size string
		fmag string
	}

	var magbuf [len(magic)]byte
	if _, err := io.ReadFull(f, magbuf[:]); err != nil {
		t.Errorf("%s: archive too short", arname)
		return
	}
	if string(magbuf[:]) != magic {
		t.Errorf("%s: incorrect archive magic string %q", arname, magbuf)
	}

	off := int64(len(magic))
	for {
		if off&1 != 0 {
			var b [1]byte
			if _, err := f.Read(b[:]); err != nil {
				if err == io.EOF {
					break
				}
				t.Errorf("%s: error skipping alignment byte at %d: %v", arname, off, err)
			}
			off++
		}

		var hdrbuf [hdrlen]byte
		if _, err := io.ReadFull(f, hdrbuf[:]); err != nil {
			if err == io.EOF {
				break
			}
			t.Errorf("%s: error reading archive header at %d: %v", arname, off, err)
			return
		}

		var hdr arhdr
		hdrslice := hdrbuf[:]
		set := func(len int, ps *string) {
			*ps = string(bytes.TrimSpace(hdrslice[:len]))
			hdrslice = hdrslice[len:]
		}
		set(namelen, &hdr.name)
		set(datelen, &hdr.date)
		set(uidlen, &hdr.uid)
		set(gidlen, &hdr.gid)
		set(modelen, &hdr.mode)
		set(sizelen, &hdr.size)
		hdr.fmag = string(hdrslice[:fmaglen])
		hdrslice = hdrslice[fmaglen:]
		if len(hdrslice) != 0 {
			t.Fatalf("internal error: len(hdrslice) == %d", len(hdrslice))
		}

		if hdr.fmag != fmag {
			t.Errorf("%s: invalid fmagic value %q at %d", arname, hdr.fmag, off)
			return
		}

		size, err := strconv.ParseInt(hdr.size, 10, 64)
		if err != nil {
			t.Errorf("%s: error parsing size %q at %d: %v", arname, hdr.size, off, err)
			return
		}

		off += hdrlen

		switch hdr.name {
		case "__.SYMDEF", "/", "/SYM64/":
			// The archive symbol map.
		case "//", "ARFILENAMES/":
			// The extended name table.
		default:
			// This should be an ELF object.
			checkELFArchiveObject(t, arname, off, io.NewSectionReader(f, off, size))
		}

		off += size
		if _, err := f.Seek(off, io.SeekStart); err != nil {
			t.Errorf("%s: failed to seek to %d: %v", arname, off, err)
		}
	}
}

// checkELFArchiveObject checks an object in an ELF archive.
func checkELFArchiveObject(t *testing.T, arname string, off int64, obj io.ReaderAt) {
	t.Helper()

	ef, err := elf.NewFile(obj)
	if err != nil {
		t.Errorf("%s: failed to open ELF file at %d: %v", arname, off, err)
		return
	}
	defer ef.Close()

	// Verify section types.
	for _, sec := range ef.Sections {
		want := elf.SHT_NULL
		switch sec.Name {
		case ".text", ".data":
			want = elf.SHT_PROGBITS
		case ".bss":
			want = elf.SHT_NOBITS
		case ".symtab":
			want = elf.SHT_SYMTAB
		case ".strtab":
			want = elf.SHT_STRTAB
		case ".init_array":
			want = elf.SHT_INIT_ARRAY
		case ".fini_array":
			want = elf.SHT_FINI_ARRAY
		case ".preinit_array":
			want = elf.SHT_PREINIT_ARRAY
		}
		if want != elf.SHT_NULL && sec.Type != want {
			t.Errorf("%s: incorrect section type in elf file at %d for section %q: got %v want %v", arname, off, sec.Name, sec.Type, want)
		}
	}
}

func TestInstall(t *testing.T) {
	if !testWork {
		defer os.RemoveAll(filepath.Join(GOPATH, "pkg"))
	}

	libgoa := "libgo.a"
	if runtime.Compiler == "gccgo" {
		libgoa = "liblibgo.a"
	}

	// Generate the p.h header file.
	//
	// 'go install -i -buildmode=c-archive ./libgo' would do that too, but that
	// would also attempt to install transitive standard-library dependencies to
	// GOROOT, and we cannot assume that GOROOT is writable. (A non-root user may
	// be running this test in a GOROOT owned by root.)
	genHeader(t, "p.h", "./p")

	testInstall(t, "./testp1"+exeSuffix,
		filepath.Join(libgodir, libgoa),
		filepath.Join(libgodir, "libgo.h"),
		"go", "install", "-buildmode=c-archive", "./libgo")

	// Test building libgo other than installing it.
	// Header files are now present.
	testInstall(t, "./testp2"+exeSuffix, "libgo.a", "libgo.h",
		"go", "build", "-buildmode=c-archive", filepath.Join(".", "libgo", "libgo.go"))

	testInstall(t, "./testp3"+exeSuffix, "libgo.a", "libgo.h",
		"go", "build", "-buildmode=c-archive", "-o", "libgo.a", "./libgo")
}

func TestEarlySignalHandler(t *testing.T) {
	switch GOOS {
	case "darwin", "ios":
		switch GOARCH {
		case "arm64":
			t.Skipf("skipping on %s/%s; see https://golang.org/issue/13701", GOOS, GOARCH)
		}
	case "windows":
		t.Skip("skipping signal test on Windows")
	}

	if !testWork {
		defer func() {
			os.Remove("libgo2.a")
			os.Remove("libgo2.h")
			os.Remove("testp" + exeSuffix)
			os.RemoveAll(filepath.Join(GOPATH, "pkg"))
		}()
	}

	cmd := exec.Command("go", "build", "-buildmode=c-archive", "-o", "libgo2.a", "./libgo2")
	if out, err := cmd.CombinedOutput(); err != nil {
		t.Logf("%s", out)
		t.Fatal(err)
	}
	checkLineComments(t, "libgo2.h")
	checkArchive(t, "libgo2.a")

	ccArgs := append(cc, "-o", "testp"+exeSuffix, "main2.c", "libgo2.a")
	if runtime.Compiler == "gccgo" {
		ccArgs = append(ccArgs, "-lgo")
	}
	if out, err := exec.Command(ccArgs[0], ccArgs[1:]...).CombinedOutput(); err != nil {
		t.Logf("%s", out)
		t.Fatal(err)
	}

	darwin := "0"
	if runtime.GOOS == "darwin" {
		darwin = "1"
	}
	cmd = exec.Command(bin[0], append(bin[1:], darwin)...)

	if out, err := cmd.CombinedOutput(); err != nil {
		t.Logf("%s", out)
		t.Fatal(err)
	}
}

func TestSignalForwarding(t *testing.T) {
	checkSignalForwardingTest(t)
	buildSignalForwardingTest(t)

	cmd := exec.Command(bin[0], append(bin[1:], "1")...)

	out, err := cmd.CombinedOutput()
	t.Logf("%v\n%s", cmd.Args, out)
	expectSignal(t, err, syscall.SIGSEGV, 0)

	// SIGPIPE is never forwarded on darwin. See golang.org/issue/33384.
	if runtime.GOOS != "darwin" && runtime.GOOS != "ios" {
		// Test SIGPIPE forwarding
		cmd = exec.Command(bin[0], append(bin[1:], "3")...)

		out, err = cmd.CombinedOutput()
		if len(out) > 0 {
			t.Logf("%s", out)
		}
		expectSignal(t, err, syscall.SIGPIPE, 0)
	}
}

func TestSignalForwardingExternal(t *testing.T) {
	if GOOS == "freebsd" || GOOS == "aix" {
		t.Skipf("skipping on %s/%s; signal always goes to the Go runtime", GOOS, GOARCH)
	} else if GOOS == "darwin" && GOARCH == "amd64" {
		t.Skipf("skipping on %s/%s: runtime does not permit SI_USER SIGSEGV", GOOS, GOARCH)
	}
	checkSignalForwardingTest(t)
	buildSignalForwardingTest(t)

	// We want to send the process a signal and see if it dies.
	// Normally the signal goes to the C thread, the Go signal
	// handler picks it up, sees that it is running in a C thread,
	// and the program dies. Unfortunately, occasionally the
	// signal is delivered to a Go thread, which winds up
	// discarding it because it was sent by another program and
	// there is no Go handler for it. To avoid this, run the
	// program several times in the hopes that it will eventually
	// fail.
	const tries = 20
	for i := 0; i < tries; i++ {
		err := runSignalForwardingTest(t, "2")
		if err == nil {
			continue
		}

		// If the signal is delivered to a C thread, as expected,
		// the Go signal handler will disable itself and re-raise
		// the signal, causing the program to die with SIGSEGV.
		//
		// It is also possible that the signal will be
		// delivered to a Go thread, such as a GC thread.
		// Currently when the Go runtime sees that a SIGSEGV was
		// sent from a different program, it first tries to send
		// the signal to the os/signal API. If nothing is looking
		// for (or explicitly ignoring) SIGSEGV, then it crashes.
		// Because the Go runtime is invoked via a c-archive,
		// it treats this as GOTRACEBACK=crash, meaning that it
		// dumps a stack trace for all goroutines, which it does
		// by raising SIGQUIT. The effect is that we will see the
		// program die with SIGQUIT in that case, not SIGSEGV.
		if expectSignal(t, err, syscall.SIGSEGV, syscall.SIGQUIT) {
			return
		}
	}

	t.Errorf("program succeeded unexpectedly %d times", tries)
}

func TestSignalForwardingGo(t *testing.T) {
	// This test fails on darwin-amd64 because of the special
	// handling of user-generated SIGSEGV signals in fixsigcode in
	// runtime/signal_darwin_amd64.go.
	if runtime.GOOS == "darwin" && runtime.GOARCH == "amd64" {
		t.Skip("not supported on darwin-amd64")
	}

	checkSignalForwardingTest(t)
	buildSignalForwardingTest(t)
	err := runSignalForwardingTest(t, "4")

	// Occasionally the signal will be delivered to a C thread,
	// and the program will crash with SIGSEGV.
	expectSignal(t, err, syscall.SIGQUIT, syscall.SIGSEGV)
}

// checkSignalForwardingTest calls t.Skip if the SignalForwarding test
// doesn't work on this platform.
func checkSignalForwardingTest(t *testing.T) {
	switch GOOS {
	case "darwin", "ios":
		switch GOARCH {
		case "arm64":
			t.Skipf("skipping on %s/%s; see https://golang.org/issue/13701", GOOS, GOARCH)
		}
	case "windows":
		t.Skip("skipping signal test on Windows")
	}
}

// buildSignalForwardingTest builds the executable used by the various
// signal forwarding tests.
func buildSignalForwardingTest(t *testing.T) {
	if !testWork {
		t.Cleanup(func() {
			os.Remove("libgo2.a")
			os.Remove("libgo2.h")
			os.Remove("testp" + exeSuffix)
			os.RemoveAll(filepath.Join(GOPATH, "pkg"))
		})
	}

	t.Log("go build -buildmode=c-archive -o libgo2.a ./libgo2")
	cmd := exec.Command("go", "build", "-buildmode=c-archive", "-o", "libgo2.a", "./libgo2")
	out, err := cmd.CombinedOutput()
	if len(out) > 0 {
		t.Logf("%s", out)
	}
	if err != nil {
		t.Fatal(err)
	}

	checkLineComments(t, "libgo2.h")
	checkArchive(t, "libgo2.a")

	ccArgs := append(cc, "-o", "testp"+exeSuffix, "main5.c", "libgo2.a")
	if runtime.Compiler == "gccgo" {
		ccArgs = append(ccArgs, "-lgo")
	}
	t.Log(ccArgs)
	out, err = exec.Command(ccArgs[0], ccArgs[1:]...).CombinedOutput()
	if len(out) > 0 {
		t.Logf("%s", out)
	}
	if err != nil {
		t.Fatal(err)
	}
}

func runSignalForwardingTest(t *testing.T, arg string) error {
	t.Logf("%v %s", bin, arg)
	cmd := exec.Command(bin[0], append(bin[1:], arg)...)

	var out strings.Builder
	cmd.Stdout = &out

	stderr, err := cmd.StderrPipe()
	if err != nil {
		t.Fatal(err)
	}
	defer stderr.Close()

	r := bufio.NewReader(stderr)

	err = cmd.Start()
	if err != nil {
		t.Fatal(err)
	}

	// Wait for trigger to ensure that process is started.
	ok, err := r.ReadString('\n')

	// Verify trigger.
	if err != nil || ok != "OK\n" {
		t.Fatal("Did not receive OK signal")
	}

	var wg sync.WaitGroup
	wg.Add(1)
	var errsb strings.Builder
	go func() {
		defer wg.Done()
		io.Copy(&errsb, r)
	}()

	// Give the program a chance to enter the function.
	// If the program doesn't get there the test will still
	// pass, although it doesn't quite test what we intended.
	// This is fine as long as the program normally makes it.
	time.Sleep(time.Millisecond)

	cmd.Process.Signal(syscall.SIGSEGV)

	err = cmd.Wait()

	s := out.String()
	if len(s) > 0 {
		t.Log(s)
	}
	wg.Wait()
	s = errsb.String()
	if len(s) > 0 {
		t.Log(s)
	}

	return err
}

// expectSignal checks that err, the exit status of a test program,
// shows a failure due to a specific signal or two. Returns whether we
// found an expected signal.
func expectSignal(t *testing.T, err error, sig1, sig2 syscall.Signal) bool {
	t.Helper()
	if err == nil {
		t.Error("test program succeeded unexpectedly")
	} else if ee, ok := err.(*exec.ExitError); !ok {
		t.Errorf("error (%v) has type %T; expected exec.ExitError", err, err)
	} else if ws, ok := ee.Sys().(syscall.WaitStatus); !ok {
		t.Errorf("error.Sys (%v) has type %T; expected syscall.WaitStatus", ee.Sys(), ee.Sys())
	} else if !ws.Signaled() || (ws.Signal() != sig1 && ws.Signal() != sig2) {
		if sig2 == 0 {
			t.Errorf("got %q; expected signal %q", ee, sig1)
		} else {
			t.Errorf("got %q; expected signal %q or %q", ee, sig1, sig2)
		}
	} else {
		return true
	}
	return false
}

func TestOsSignal(t *testing.T) {
	switch GOOS {
	case "windows":
		t.Skip("skipping signal test on Windows")
	}

	if !testWork {
		defer func() {
			os.Remove("libgo3.a")
			os.Remove("libgo3.h")
			os.Remove("testp" + exeSuffix)
			os.RemoveAll(filepath.Join(GOPATH, "pkg"))
		}()
	}

	cmd := exec.Command("go", "build", "-buildmode=c-archive", "-o", "libgo3.a", "./libgo3")
	if out, err := cmd.CombinedOutput(); err != nil {
		t.Logf("%s", out)
		t.Fatal(err)
	}
	checkLineComments(t, "libgo3.h")
	checkArchive(t, "libgo3.a")

	ccArgs := append(cc, "-o", "testp"+exeSuffix, "main3.c", "libgo3.a")
	if runtime.Compiler == "gccgo" {
		ccArgs = append(ccArgs, "-lgo")
	}
	if out, err := exec.Command(ccArgs[0], ccArgs[1:]...).CombinedOutput(); err != nil {
		t.Logf("%s", out)
		t.Fatal(err)
	}

	if out, err := exec.Command(bin[0], bin[1:]...).CombinedOutput(); err != nil {
		t.Logf("%s", out)
		t.Fatal(err)
	}
}

func TestSigaltstack(t *testing.T) {
	switch GOOS {
	case "windows":
		t.Skip("skipping signal test on Windows")
	}

	if !testWork {
		defer func() {
			os.Remove("libgo4.a")
			os.Remove("libgo4.h")
			os.Remove("testp" + exeSuffix)
			os.RemoveAll(filepath.Join(GOPATH, "pkg"))
		}()
	}

	cmd := exec.Command("go", "build", "-buildmode=c-archive", "-o", "libgo4.a", "./libgo4")
	if out, err := cmd.CombinedOutput(); err != nil {
		t.Logf("%s", out)
		t.Fatal(err)
	}
	checkLineComments(t, "libgo4.h")
	checkArchive(t, "libgo4.a")

	ccArgs := append(cc, "-o", "testp"+exeSuffix, "main4.c", "libgo4.a")
	if runtime.Compiler == "gccgo" {
		ccArgs = append(ccArgs, "-lgo")
	}
	if out, err := exec.Command(ccArgs[0], ccArgs[1:]...).CombinedOutput(); err != nil {
		t.Logf("%s", out)
		t.Fatal(err)
	}

	if out, err := exec.Command(bin[0], bin[1:]...).CombinedOutput(); err != nil {
		t.Logf("%s", out)
		t.Fatal(err)
	}
}

const testar = `#!/usr/bin/env bash
while [[ $1 == -* ]] >/dev/null; do
  shift
done
echo "testar" > $1
echo "testar" > PWD/testar.ran
`

func TestExtar(t *testing.T) {
	switch GOOS {
	case "windows":
		t.Skip("skipping signal test on Windows")
	}
	if runtime.Compiler == "gccgo" {
		t.Skip("skipping -extar test when using gccgo")
	}
	if runtime.GOOS == "ios" {
		t.Skip("shell scripts are not executable on iOS hosts")
	}

	if !testWork {
		defer func() {
			os.Remove("libgo4.a")
			os.Remove("libgo4.h")
			os.Remove("testar")
			os.Remove("testar.ran")
			os.RemoveAll(filepath.Join(GOPATH, "pkg"))
		}()
	}

	os.Remove("testar")
	dir, err := os.Getwd()
	if err != nil {
		t.Fatal(err)
	}
	s := strings.Replace(testar, "PWD", dir, 1)
	if err := os.WriteFile("testar", []byte(s), 0777); err != nil {
		t.Fatal(err)
	}

	cmd := exec.Command("go", "build", "-buildmode=c-archive", "-ldflags=-extar="+filepath.Join(dir, "testar"), "-o", "libgo4.a", "./libgo4")
	if out, err := cmd.CombinedOutput(); err != nil {
		t.Logf("%s", out)
		t.Fatal(err)
	}
	checkLineComments(t, "libgo4.h")

	if _, err := os.Stat("testar.ran"); err != nil {
		if os.IsNotExist(err) {
			t.Error("testar does not exist after go build")
		} else {
			t.Errorf("error checking testar: %v", err)
		}
	}
}

func TestPIE(t *testing.T) {
	switch GOOS {
	case "windows", "darwin", "ios", "plan9":
		t.Skipf("skipping PIE test on %s", GOOS)
	}

	libgoa := "libgo.a"
	if runtime.Compiler == "gccgo" {
		libgoa = "liblibgo.a"
	}

	if !testWork {
		defer func() {
			os.Remove("testp" + exeSuffix)
			os.Remove(libgoa)
			os.RemoveAll(filepath.Join(GOPATH, "pkg"))
		}()
	}

	// Generate the p.h header file.
	//
	// 'go install -i -buildmode=c-archive ./libgo' would do that too, but that
	// would also attempt to install transitive standard-library dependencies to
	// GOROOT, and we cannot assume that GOROOT is writable. (A non-root user may
	// be running this test in a GOROOT owned by root.)
	genHeader(t, "p.h", "./p")

	cmd := exec.Command("go", "build", "-buildmode=c-archive", "./libgo")
	if out, err := cmd.CombinedOutput(); err != nil {
		t.Logf("%s", out)
		t.Fatal(err)
	}

	ccArgs := append(cc, "-fPIE", "-pie", "-o", "testp"+exeSuffix, "main.c", "main_unix.c", libgoa)
	if runtime.Compiler == "gccgo" {
		ccArgs = append(ccArgs, "-lgo")
	}
	if out, err := exec.Command(ccArgs[0], ccArgs[1:]...).CombinedOutput(); err != nil {
		t.Logf("%s", out)
		t.Fatal(err)
	}

	binArgs := append(bin, "arg1", "arg2")
	cmd = exec.Command(binArgs[0], binArgs[1:]...)
	if runtime.Compiler == "gccgo" {
		cmd.Env = append(os.Environ(), "GCCGO=1")
	}
	if out, err := cmd.CombinedOutput(); err != nil {
		t.Logf("%s", out)
		t.Fatal(err)
	}

	if GOOS != "aix" {
		f, err := elf.Open("testp" + exeSuffix)
		if err != nil {
			t.Fatal("elf.Open failed: ", err)
		}
		defer f.Close()
		if hasDynTag(t, f, elf.DT_TEXTREL) {
			t.Errorf("%s has DT_TEXTREL flag", "testp"+exeSuffix)
		}
	}
}

func hasDynTag(t *testing.T, f *elf.File, tag elf.DynTag) bool {
	ds := f.SectionByType(elf.SHT_DYNAMIC)
	if ds == nil {
		t.Error("no SHT_DYNAMIC section")
		return false
	}
	d, err := ds.Data()
	if err != nil {
		t.Errorf("can't read SHT_DYNAMIC contents: %v", err)
		return false
	}
	for len(d) > 0 {
		var t elf.DynTag
		switch f.Class {
		case elf.ELFCLASS32:
			t = elf.DynTag(f.ByteOrder.Uint32(d[:4]))
			d = d[8:]
		case elf.ELFCLASS64:
			t = elf.DynTag(f.ByteOrder.Uint64(d[:8]))
			d = d[16:]
		}
		if t == tag {
			return true
		}
	}
	return false
}

func TestSIGPROF(t *testing.T) {
	switch GOOS {
	case "windows", "plan9":
		t.Skipf("skipping SIGPROF test on %s", GOOS)
	case "darwin", "ios":
		t.Skipf("skipping SIGPROF test on %s; see https://golang.org/issue/19320", GOOS)
	}

	t.Parallel()

	if !testWork {
		defer func() {
			os.Remove("testp6" + exeSuffix)
			os.Remove("libgo6.a")
			os.Remove("libgo6.h")
		}()
	}

	cmd := exec.Command("go", "build", "-buildmode=c-archive", "-o", "libgo6.a", "./libgo6")
	out, err := cmd.CombinedOutput()
	t.Logf("%v\n%s", cmd.Args, out)
	if err != nil {
		t.Fatal(err)
	}
	checkLineComments(t, "libgo6.h")
	checkArchive(t, "libgo6.a")

	ccArgs := append(cc, "-o", "testp6"+exeSuffix, "main6.c", "libgo6.a")
	if runtime.Compiler == "gccgo" {
		ccArgs = append(ccArgs, "-lgo")
	}
	out, err = exec.Command(ccArgs[0], ccArgs[1:]...).CombinedOutput()
	t.Logf("%v\n%s", ccArgs, out)
	if err != nil {
		t.Fatal(err)
	}

	argv := cmdToRun("./testp6")
	cmd = exec.Command(argv[0], argv[1:]...)
	out, err = cmd.CombinedOutput()
	t.Logf("%v\n%s", argv, out)
	if err != nil {
		t.Fatal(err)
	}
}

// TestCompileWithoutShared tests that if we compile code without the
// -shared option, we can put it into an archive. When we use the go
// tool with -buildmode=c-archive, it passes -shared to the compiler,
// so we override that. The go tool doesn't work this way, but Bazel
// will likely do it in the future. And it ought to work. This test
// was added because at one time it did not work on PPC Linux.
func TestCompileWithoutShared(t *testing.T) {
	// For simplicity, reuse the signal forwarding test.
	checkSignalForwardingTest(t)

	if !testWork {
		defer func() {
			os.Remove("libgo2.a")
			os.Remove("libgo2.h")
		}()
	}

	cmd := exec.Command("go", "build", "-buildmode=c-archive", "-gcflags=-shared=false", "-o", "libgo2.a", "./libgo2")
	out, err := cmd.CombinedOutput()
	t.Logf("%v\n%s", cmd.Args, out)
	if err != nil {
		t.Fatal(err)
	}
	checkLineComments(t, "libgo2.h")
	checkArchive(t, "libgo2.a")

	exe := "./testnoshared" + exeSuffix

	// In some cases, -no-pie is needed here, but not accepted everywhere. First try
	// if -no-pie is accepted. See #22126.
	ccArgs := append(cc, "-o", exe, "-no-pie", "main5.c", "libgo2.a")
	if runtime.Compiler == "gccgo" {
		ccArgs = append(ccArgs, "-lgo")
	}
	out, err = exec.Command(ccArgs[0], ccArgs[1:]...).CombinedOutput()
	t.Logf("%v\n%s", ccArgs, out)

	// If -no-pie unrecognized, try -nopie if this is possibly clang
	if err != nil && bytes.Contains(out, []byte("unknown")) && !strings.Contains(cc[0], "gcc") {
		ccArgs = append(cc, "-o", exe, "-nopie", "main5.c", "libgo2.a")
		out, err = exec.Command(ccArgs[0], ccArgs[1:]...).CombinedOutput()
		t.Logf("%v\n%s", ccArgs, out)
	}

	// Don't use either -no-pie or -nopie
	if err != nil && bytes.Contains(out, []byte("unrecognized")) {
		ccArgs = append(cc, "-o", exe, "main5.c", "libgo2.a")
		out, err = exec.Command(ccArgs[0], ccArgs[1:]...).CombinedOutput()
		t.Logf("%v\n%s", ccArgs, out)
	}
	if err != nil {
		t.Fatal(err)
	}
	if !testWork {
		defer os.Remove(exe)
	}

	binArgs := append(cmdToRun(exe), "1")
	out, err = exec.Command(binArgs[0], binArgs[1:]...).CombinedOutput()
	t.Logf("%v\n%s", binArgs, out)
	expectSignal(t, err, syscall.SIGSEGV, 0)

	// SIGPIPE is never forwarded on darwin. See golang.org/issue/33384.
	if runtime.GOOS != "darwin" && runtime.GOOS != "ios" {
		binArgs := append(cmdToRun(exe), "3")
		out, err = exec.Command(binArgs[0], binArgs[1:]...).CombinedOutput()
		t.Logf("%v\n%s", binArgs, out)
		expectSignal(t, err, syscall.SIGPIPE, 0)
	}
}

// Test that installing a second time recreates the header file.
func TestCachedInstall(t *testing.T) {
	if !testWork {
		defer os.RemoveAll(filepath.Join(GOPATH, "pkg"))
	}

	h := filepath.Join(libgodir, "libgo.h")

	buildcmd := []string{"go", "install", "-buildmode=c-archive", "./libgo"}

	cmd := exec.Command(buildcmd[0], buildcmd[1:]...)
	cmd.Env = append(cmd.Environ(), "GO111MODULE=off") // 'go install' only works in GOPATH mode
	t.Log(buildcmd)
	if out, err := cmd.CombinedOutput(); err != nil {
		t.Logf("%s", out)
		t.Fatal(err)
	}

	if _, err := os.Stat(h); err != nil {
		t.Errorf("libgo.h not installed: %v", err)
	}

	if err := os.Remove(h); err != nil {
		t.Fatal(err)
	}

	cmd = exec.Command(buildcmd[0], buildcmd[1:]...)
	cmd.Env = append(cmd.Environ(), "GO111MODULE=off")
	t.Log(buildcmd)
	if out, err := cmd.CombinedOutput(); err != nil {
		t.Logf("%s", out)
		t.Fatal(err)
	}

	if _, err := os.Stat(h); err != nil {
		t.Errorf("libgo.h not installed in second run: %v", err)
	}
}

// Issue 35294.
func TestManyCalls(t *testing.T) {
	t.Parallel()

	if !testWork {
		defer func() {
			os.Remove("testp7" + exeSuffix)
			os.Remove("libgo7.a")
			os.Remove("libgo7.h")
		}()
	}

	cmd := exec.Command("go", "build", "-buildmode=c-archive", "-o", "libgo7.a", "./libgo7")
	out, err := cmd.CombinedOutput()
	t.Logf("%v\n%s", cmd.Args, out)
	if err != nil {
		t.Fatal(err)
	}
	checkLineComments(t, "libgo7.h")
	checkArchive(t, "libgo7.a")

	ccArgs := append(cc, "-o", "testp7"+exeSuffix, "main7.c", "libgo7.a")
	if runtime.Compiler == "gccgo" {
		ccArgs = append(ccArgs, "-lgo")
	}
	out, err = exec.Command(ccArgs[0], ccArgs[1:]...).CombinedOutput()
	t.Logf("%v\n%s", ccArgs, out)
	if err != nil {
		t.Fatal(err)
	}

	argv := cmdToRun("./testp7")
	cmd = exec.Command(argv[0], argv[1:]...)
	sb := new(strings.Builder)
	cmd.Stdout = sb
	cmd.Stderr = sb
	if err := cmd.Start(); err != nil {
		t.Fatal(err)
	}

	timer := time.AfterFunc(time.Minute,
		func() {
			t.Error("test program timed out")
			cmd.Process.Kill()
		},
	)
	defer timer.Stop()

	err = cmd.Wait()
	t.Logf("%v\n%s", cmd.Args, sb)
	if err != nil {
		t.Error(err)
	}
}

// Issue 49288.
func TestPreemption(t *testing.T) {
	if runtime.Compiler == "gccgo" {
		t.Skip("skipping asynchronous preemption test with gccgo")
	}

	t.Parallel()

	if !testWork {
		defer func() {
			os.Remove("testp8" + exeSuffix)
			os.Remove("libgo8.a")
			os.Remove("libgo8.h")
		}()
	}

	cmd := exec.Command("go", "build", "-buildmode=c-archive", "-o", "libgo8.a", "./libgo8")
	out, err := cmd.CombinedOutput()
	t.Logf("%v\n%s", cmd.Args, out)
	if err != nil {
		t.Fatal(err)
	}
	checkLineComments(t, "libgo8.h")
	checkArchive(t, "libgo8.a")

	ccArgs := append(cc, "-o", "testp8"+exeSuffix, "main8.c", "libgo8.a")
	out, err = exec.Command(ccArgs[0], ccArgs[1:]...).CombinedOutput()
	t.Logf("%v\n%s", ccArgs, out)
	if err != nil {
		t.Fatal(err)
	}

	argv := cmdToRun("./testp8")
	cmd = exec.Command(argv[0], argv[1:]...)
	sb := new(strings.Builder)
	cmd.Stdout = sb
	cmd.Stderr = sb
	if err := cmd.Start(); err != nil {
		t.Fatal(err)
	}

	timer := time.AfterFunc(time.Minute,
		func() {
			t.Error("test program timed out")
			cmd.Process.Kill()
		},
	)
	defer timer.Stop()

	err = cmd.Wait()
	t.Logf("%v\n%s", cmd.Args, sb)
	if err != nil {
		t.Error(err)
	}
}
