// Copyright 2023 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 runtime_test

import (
	"internal/testenv"
	"os"
	"os/exec"
	"reflect"
	"runtime"
	"strconv"
	"strings"
	"testing"
)

// This is the function we'll be testing.
// It has a simple write barrier in it.
func setGlobalPointer() {
	globalPointer = nil
}

var globalPointer *int

func TestUnsafePoint(t *testing.T) {
	testenv.MustHaveExec(t)
	switch runtime.GOARCH {
	case "amd64", "arm64":
	default:
		t.Skipf("test not enabled for %s", runtime.GOARCH)
	}

	// Get a reference we can use to ask the runtime about
	// which of its instructions are unsafe preemption points.
	f := runtime.FuncForPC(reflect.ValueOf(setGlobalPointer).Pointer())

	// Disassemble the test function.
	// Note that normally "go test runtime" would strip symbols
	// and prevent this step from working. So there's a hack in
	// cmd/go/internal/test that exempts runtime tests from
	// symbol stripping.
	cmd := exec.Command(testenv.GoToolPath(t), "tool", "objdump", "-s", "setGlobalPointer", os.Args[0])
	out, err := cmd.CombinedOutput()
	if err != nil {
		t.Fatalf("can't objdump %v", err)
	}
	lines := strings.Split(string(out), "\n")[1:]

	// Walk through assembly instructions, checking preemptible flags.
	var entry uint64
	var startedWB bool
	var doneWB bool
	instructionCount := 0
	unsafeCount := 0
	for _, line := range lines {
		line = strings.TrimSpace(line)
		t.Logf("%s", line)
		parts := strings.Fields(line)
		if len(parts) < 4 {
			continue
		}
		if !strings.HasPrefix(parts[0], "unsafepoint_test.go:") {
			continue
		}
		pc, err := strconv.ParseUint(parts[1][2:], 16, 64)
		if err != nil {
			t.Fatalf("can't parse pc %s: %v", parts[1], err)
		}
		if entry == 0 {
			entry = pc
		}
		// Note that some platforms do ASLR, so the PCs in the disassembly
		// don't match PCs in the address space. Only offsets from function
		// entry make sense.
		unsafe := runtime.UnsafePoint(f.Entry() + uintptr(pc-entry))
		t.Logf("unsafe: %v\n", unsafe)
		instructionCount++
		if unsafe {
			unsafeCount++
		}

		// All the instructions inside the write barrier must be unpreemptible.
		if startedWB && !doneWB && !unsafe {
			t.Errorf("instruction %s must be marked unsafe, but isn't", parts[1])
		}

		// Detect whether we're in the write barrier.
		switch runtime.GOARCH {
		case "arm64":
			if parts[3] == "MOVWU" {
				// The unpreemptible region starts after the
				// load of runtime.writeBarrier.
				startedWB = true
			}
			if parts[3] == "MOVD" && parts[4] == "ZR," {
				// The unpreemptible region ends after the
				// write of nil.
				doneWB = true
			}
		case "amd64":
			if parts[3] == "CMPL" {
				startedWB = true
			}
			if parts[3] == "MOVQ" && parts[4] == "$0x0," {
				doneWB = true
			}
		}
	}

	if instructionCount == 0 {
		t.Errorf("no instructions")
	}
	if unsafeCount == instructionCount {
		t.Errorf("no interruptible instructions")
	}
	// Note that there are other instructions marked unpreemptible besides
	// just the ones required by the write barrier. Those include possibly
	// the preamble and postamble, as well as bleeding out from the
	// write barrier proper into adjacent instructions (in both directions).
	// Hopefully we can clean up the latter at some point.
}
