test: expand reliability regression coverage
Some checks failed
build / trigger-build-image (push) Blocked by required conditions
build / build (push) Has been cancelled
build / build (pull_request) Successful in 2m7s
build / trigger-build-image (pull_request) Has been skipped

This commit is contained in:
张泊明518370910136 2026-08-02 04:28:24 -07:00
parent 79a64efbfd
commit e5de5b2bd6
GPG Key ID: D47306D7062CDA9D
15 changed files with 705 additions and 41 deletions

View File

@ -8,6 +8,7 @@ DATE := $(shell date +"%Y%m%d-%H%M%S")
VERSION := $(COMMIT_HASH)-$(DATE)
LDFLAGS := -s -w -X main.Version=$(VERSION)
GOFLAGS := -trimpath -mod=readonly -buildvcs=false
COVERAGE_FILE ?= coverage.out
all: build
@ -27,11 +28,12 @@ lint:
prepare-test:
git submodule update --init --remote
test:
test: build
./scripts/prepare_test_repos.sh $(TMP_DIR)
# no clang-tidy-18 locally
rm -rf $(TMP_DIR)/submodules/JOJ3-examples/examples/keyword/clangtidy
go test -count=1 -v ./...
go test -count=1 -v -coverpkg=./... -coverprofile=$(COVERAGE_FILE) ./...
go tool cover -func=$(COVERAGE_FILE) | tail -n 1
local-test:
rm -rf $(TMP_DIR)/submodules/JOJ3-examples/examples/

View File

@ -175,6 +175,9 @@ func GetConfPath(confRoot, confName, fallbackConfName, msg, tag string) (
hintValidScopes(confRoot, confName)
}
slog.Error("stat conf", "error", err)
if tag != "" {
return confPath, confStat, conventionalCommit, err
}
// fallback to conf file in conf root on conf not exist
confPath = filepath.Join(confRoot, fallbackConfName)
slog.Info("fallback to conf", "path", confPath)

View File

@ -1,10 +1,67 @@
package conf
import (
"crypto/sha256"
"encoding/hex"
"os"
"path/filepath"
"reflect"
"strings"
"testing"
)
func TestGetSHA256(t *testing.T) {
path := filepath.Join(t.TempDir(), "input")
content := []byte("joj3")
if err := os.WriteFile(path, content, 0o600); err != nil {
t.Fatal(err)
}
wantSum := sha256.Sum256(content)
got, err := GetSHA256(path)
if err != nil || got != hex.EncodeToString(wantSum[:]) {
t.Fatalf("GetSHA256() = %q, %v", got, err)
}
if _, err := GetSHA256(path + ".missing"); !os.IsNotExist(err) {
t.Fatalf("missing GetSHA256() error = %v", err)
}
}
func TestGetConfPath(t *testing.T) {
root := t.TempDir()
scopedDir := filepath.Join(root, "course")
if err := os.Mkdir(scopedDir, 0o700); err != nil {
t.Fatal(err)
}
for _, path := range []string{filepath.Join(scopedDir, "conf.json"), filepath.Join(root, "fallback.json")} {
if err := os.WriteFile(path, []byte("{}"), 0o600); err != nil {
t.Fatal(err)
}
}
got, _, commit, err := GetConfPath(root, "conf.json", "fallback.json", "test(course): run", "")
if err != nil || got != filepath.Join(scopedDir, "conf.json") || commit.Scope != "course" {
t.Fatalf("GetConfPath(scoped) = %q, %+v, %v", got, commit, err)
}
got, _, _, err = GetConfPath(root, "conf.json", "fallback.json", "not conventional", "")
if err != nil || got != filepath.Join(root, "fallback.json") {
t.Fatalf("GetConfPath(fallback) = %q, %v", got, err)
}
if _, _, err = parseMsg(root, "conf.json", "test(../escape): run", ""); err == nil || !strings.Contains(err.Error(), "invalid scope") {
t.Fatalf("parseMsg(traversal) error = %v", err)
}
got, _, commit, err = GetConfPath(root, "conf.json", "fallback.json", "ignored", "missing-tag")
if !os.IsNotExist(err) || got != filepath.Join(root, "missing-tag", "conf.json") || commit.Scope != "missing-tag" {
t.Fatalf("GetConfPath(tag) = %q, %+v, %v", got, commit, err)
}
if err := os.Remove(filepath.Join(root, "fallback.json")); err != nil {
t.Fatal(err)
}
got, _, _, err = GetConfPath(root, "conf.json", "fallback.json", "invalid", "")
if !os.IsNotExist(err) || got != filepath.Join(root, "fallback.json") {
t.Fatalf("GetConfPath(missing fallback) = %q, %v", got, err)
}
}
func TestParseConventionalCommit(t *testing.T) {
tests := []struct {
name string

View File

@ -87,7 +87,16 @@ func TestRun(t *testing.T) {
t.Fatal(err)
}
for _, tt := range tests {
// The repo-health-checker package runs all healthcheck fixtures directly so
// their execution contributes to Go coverage. Keep one sandbox case here as
// an end-to-end binary/executor/parser smoke test.
if strings.HasPrefix(tt, "healthcheck/") && tt != "healthcheck/release" {
continue
}
t.Run(tt, func(t *testing.T) {
if tt == "healthcheck/release" {
prepareLargeCopyInFixture(t, filepath.Join(root, tt))
}
t.Chdir(fmt.Sprintf("%s%s", root, tt))
os.Args = []string{"./joj3"}
outputFile := "joj3_result.json"
@ -106,3 +115,54 @@ func TestRun(t *testing.T) {
})
}
}
func prepareLargeCopyInFixture(t *testing.T, dir string) {
t.Helper()
const fileCount = 1001
filesDir := filepath.Join(dir, "many-files")
if err := os.Mkdir(filesDir, 0o700); err != nil && !os.IsExist(err) {
t.Fatal(err)
}
for i := range fileCount {
path := filepath.Join(filesDir, fmt.Sprintf("%04d", i))
if err := os.WriteFile(path, nil, 0o600); err != nil {
t.Fatal(err)
}
}
t.Cleanup(func() { _ = os.RemoveAll(filesDir) })
confPath := filepath.Join(dir, "conf.json")
original, err := os.ReadFile(confPath)
if err != nil {
t.Fatal(err)
}
var conf map[string]any
if err := json.Unmarshal(original, &conf); err != nil {
t.Fatal(err)
}
stages := conf["stages"].([]any)
executor := stages[0].(map[string]any)["executor"].(map[string]any)
with := executor["with"].(map[string]any)
command := with["default"].(map[string]any)
args := command["args"].([]any)
checkerArgs := make([]string, 0, len(args))
for _, arg := range args {
checkerArgs = append(checkerArgs, fmt.Sprintf("%q", arg))
}
command["args"] = []string{
"/bin/sh", "-c",
fmt.Sprintf("test \"$(find many-files -type f | wc -l)\" -eq %d && exec %s", fileCount, strings.Join(checkerArgs, " ")),
}
patched, err := json.Marshal(conf)
if err != nil {
t.Fatal(err)
}
if err := os.WriteFile(confPath, patched, 0o600); err != nil {
t.Fatal(err)
}
t.Cleanup(func() {
if err := os.WriteFile(confPath, original, 0o600); err != nil {
t.Errorf("restore conf: %v", err)
}
})
}

View File

@ -7,6 +7,7 @@ import (
"encoding/json"
"flag"
"fmt"
"io"
"log/slog"
"os"
@ -15,8 +16,8 @@ import (
// parseMultiValueFlag parses a multi-value command-line flag and appends its values to the provided slice.
// It registers a flag with the specified name and description, associating it with a multiStringValue receiver.
func parseMultiValueFlag(values *[]string, flagName, description string) {
flag.Var((*multiStringValue)(values), flagName, description)
func parseMultiValueFlag(flags *flag.FlagSet, values *[]string, flagName, description string) {
flags.Var((*multiStringValue)(values), flagName, description)
}
type multiStringValue []string
@ -39,36 +40,37 @@ func setupSlog() {
slog.SetDefault(logger)
}
var (
rootDir string
repoSize float64
checkFileNameList string
checkFileSumList string
metaFile []string
whitelistedChars string
allowedDomainList string
actorCsvPath string
showVersion *bool
Version string
)
var Version string
func init() {
showVersion = flag.Bool("version", false, "print current version")
flag.StringVar(&rootDir, "root", ".", "root dir for forbidden files check")
flag.Float64Var(&repoSize, "repoSize", 2, "maximum size of the repo in MiB")
flag.StringVar(&checkFileNameList, "checkFileNameList", "", "comma-separated list of files to check")
flag.StringVar(&checkFileSumList, "checkFileSumList", "", "comma-separated list of expected checksums")
flag.StringVar(&whitelistedChars, "whitelistedChars", "", "comma-separated list of non-ASCII characters allowed in files")
flag.StringVar(&allowedDomainList, "allowedDomainList", "sjtu.edu.cn", "comma-separated list of allowed domains for commit author email")
flag.StringVar(&actorCsvPath, "actorCsvPath", "/home/tt/.config/joj/students.csv", "path to actor csv file")
parseMultiValueFlag(&metaFile, "meta", "meta files to check")
}
func main() {
flag.Parse()
if *showVersion {
fmt.Println(Version)
return
func run(args []string, stdout io.Writer) error {
flags := flag.NewFlagSet("repo-health-checker", flag.ContinueOnError)
flags.SetOutput(io.Discard)
var (
rootDir string
repoSize float64
checkFileNameList string
checkFileSumList string
metaFile []string
whitelistedChars string
allowedDomainList string
actorCsvPath string
showVersion bool
)
flags.BoolVar(&showVersion, "version", false, "print current version")
flags.StringVar(&rootDir, "root", ".", "root dir for forbidden files check")
flags.Float64Var(&repoSize, "repoSize", 2, "maximum size of the repo in MiB")
flags.StringVar(&checkFileNameList, "checkFileNameList", "", "comma-separated list of files to check")
flags.StringVar(&checkFileSumList, "checkFileSumList", "", "comma-separated list of expected checksums")
flags.StringVar(&whitelistedChars, "whitelistedChars", "", "comma-separated list of non-ASCII characters allowed in files")
flags.StringVar(&allowedDomainList, "allowedDomainList", "sjtu.edu.cn", "comma-separated list of allowed domains for commit author email")
flags.StringVar(&actorCsvPath, "actorCsvPath", "/home/tt/.config/joj/students.csv", "path to actor csv file")
parseMultiValueFlag(flags, &metaFile, "meta", "meta files to check")
if err := flags.Parse(args); err != nil {
return err
}
if showVersion {
_, err := fmt.Fprintln(stdout, Version)
return err
}
setupSlog()
slog.Info("start repo-health-checker", "version", Version)
@ -92,7 +94,15 @@ func main() {
jsonRes, err := json.Marshal(res)
if err != nil {
slog.Error("marshal result", "error", err)
return err
}
_, err = fmt.Fprintln(stdout, string(jsonRes))
return err
}
func main() {
if err := run(os.Args[1:], os.Stdout); err != nil {
slog.Error("repo-health-checker", "error", err)
os.Exit(1)
}
fmt.Println(string(jsonRes))
}

View File

@ -0,0 +1,101 @@
package main
import (
"bytes"
"encoding/json"
"os"
"path/filepath"
"reflect"
"strings"
"testing"
"github.com/joint-online-judge/JOJ3/internal/stage"
"github.com/joint-online-judge/JOJ3/pkg/healthcheck"
)
type exampleConf struct {
Stages []struct {
Executor struct {
With struct {
Default struct {
Args []string `json:"args"`
} `json:"default"`
} `json:"with"`
} `json:"executor"`
} `json:"stages"`
}
func readJSON[T any](t *testing.T, path string) T {
t.Helper()
data, err := os.ReadFile(path)
if err != nil {
t.Fatal(err)
}
var value T
if err := json.Unmarshal(data, &value); err != nil {
t.Fatal(err)
}
return value
}
func TestHealthcheckExamples(t *testing.T) {
root := "../../tmp/submodules/JOJ3-examples/examples/healthcheck"
entries, err := os.ReadDir(root)
if err != nil {
t.Fatal(err)
}
for _, entry := range entries {
if !entry.IsDir() {
continue
}
// This fixture is retained in cmd/joj3 as the sandbox binary/parser smoke
// test, so do not execute it a second time here.
if entry.Name() == "release" {
continue
}
t.Run(entry.Name(), func(t *testing.T) {
dir := filepath.Join(root, entry.Name())
conf := readJSON[exampleConf](t, filepath.Join(dir, "conf.json"))
expected := readJSON[[]stage.StageResult](t, filepath.Join(dir, "expected.json"))
if len(conf.Stages) != 1 || len(expected) != 1 || len(expected[0].Results) != 1 {
t.Fatal("healthcheck fixture must contain one stage and one result")
}
args := conf.Stages[0].Executor.With.Default.Args
if len(args) == 0 {
t.Fatal("healthcheck fixture has no command")
}
t.Chdir(dir)
var stdout bytes.Buffer
if err := run(args[1:], &stdout); err != nil {
t.Fatal(err)
}
var got healthcheck.Result
if err := json.Unmarshal(stdout.Bytes(), &got); err != nil {
t.Fatalf("decode output %q: %v", stdout.String(), err)
}
want := healthcheck.Result{
Msg: expected[0].Results[0].Comment,
Failed: expected[0].ForceQuit,
}
if !reflect.DeepEqual(got, want) {
t.Fatalf("run() = %+v, want %+v", got, want)
}
})
}
}
func TestRunVersionAndInvalidFlag(t *testing.T) {
oldVersion := Version
Version = "test-version"
t.Cleanup(func() { Version = oldVersion })
var stdout bytes.Buffer
if err := run([]string{"-version"}, &stdout); err != nil {
t.Fatal(err)
}
if strings.TrimSpace(stdout.String()) != Version {
t.Fatalf("version output = %q", stdout.String())
}
if err := run([]string{"-unknown"}, &stdout); err == nil {
t.Fatal("run() accepted an unknown flag")
}
}

View File

@ -71,14 +71,12 @@ func (e *Local) generateResult(
if err != nil {
if exitErr, ok := err.(*exec.ExitError); ok {
result.Status = stage.StatusNonzeroExitStatus
status := exitErr.Sys().(syscall.WaitStatus)
if status.Signaled() {
signal := status.Signal()
switch signal {
case syscall.SIGXCPU:
if signal == syscall.SIGXCPU {
result.Status = stage.StatusTimeLimitExceeded
default:
result.Status = stage.StatusNonzeroExitStatus
}
}
result.Error = exitErr.Error()

View File

@ -1,16 +1,120 @@
package local
import (
"bytes"
"context"
"errors"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
"time"
"github.com/joint-online-judge/JOJ3/internal/stage"
)
func stringPtr(s string) *string { return &s }
func TestRunCapturesIOAndCopyOut(t *testing.T) {
dir := t.TempDir()
output := filepath.Join(dir, "artifact")
results, err := (&Local{}).Run(context.Background(), []stage.Cmd{{
Args: []string{"/bin/sh", "-c", "read value; printf '%s:%s' \"$MARK\" \"$value\"; printf artifact > \"$1\"", "sh", output},
Env: []string{"MARK=env"},
Stdin: &stage.CmdFile{Content: stringPtr("input\n")},
Stdout: &stage.CmdFile{Name: stringPtr("stdout")},
CopyOut: []string{output, filepath.Join(dir, "optional?")},
}})
if err != nil {
t.Fatal(err)
}
if len(results) != 1 || results[0].Status != stage.StatusAccepted {
t.Fatalf("Run() results = %+v", results)
}
if got := results[0].Files["stdout"]; got != "env:input" {
t.Fatalf("stdout = %q", got)
}
if got := results[0].Files[output]; got != "artifact" {
t.Fatalf("copy-out = %q", got)
}
}
func TestGenerateResultClassifiesErrors(t *testing.T) {
result := (&Local{}).generateResult(errors.New("start failed"), nil, -time.Second,
stage.Cmd{}, bytes.Buffer{}, bytes.Buffer{}, false)
if result.Status != stage.StatusInternalError || result.ExitStatus != -1 || result.RunTime != 0 {
t.Fatalf("generateResult() = %+v", result)
}
cmd := exec.Command("/bin/sh", "-c", "exit 7")
err := cmd.Run()
result = (&Local{}).generateResult(err, cmd.ProcessState, time.Millisecond,
stage.Cmd{}, bytes.Buffer{}, bytes.Buffer{}, false)
if result.Status != stage.StatusNonzeroExitStatus || result.ExitStatus != 7 {
t.Fatalf("exit result = %+v", result)
}
}
func TestRunReportsRequiredCopyOutError(t *testing.T) {
missing := filepath.Join(t.TempDir(), "missing")
results, err := (&Local{}).Run(context.Background(), []stage.Cmd{{
Args: []string{"/bin/true"}, CopyOut: []string{missing},
}})
if err != nil {
t.Fatal(err)
}
if len(results) != 1 || results[0].Status != stage.StatusFileError || results[0].Error == "" {
t.Fatalf("Run() results = %+v", results)
}
}
func TestRunReadsStdinFileAndRunsMultipleCommands(t *testing.T) {
input := filepath.Join(t.TempDir(), "stdin")
if err := os.WriteFile(input, []byte("from-file"), 0o600); err != nil {
t.Fatal(err)
}
results, err := (&Local{}).Run(context.Background(), []stage.Cmd{
{Args: []string{"/bin/true"}},
{
Args: []string{"/bin/cat"},
Stdin: &stage.CmdFile{Src: &input},
Stdout: &stage.CmdFile{Name: stringPtr("stdout")},
},
})
if err != nil {
t.Fatal(err)
}
if len(results) != 2 || results[1].Files["stdout"] != "from-file" {
t.Fatalf("Run() results = %+v", results)
}
}
func TestRunSetupErrors(t *testing.T) {
_, err := (&Local{}).Run(context.Background(), []stage.Cmd{{
Args: []string{"/bin/cat"}, Stdin: &stage.CmdFile{Src: stringPtr(filepath.Join(t.TempDir(), "missing"))},
}})
if err == nil || !strings.Contains(err.Error(), "failed to open stdin file") {
t.Fatalf("stdin error = %v", err)
}
_, err = (&Local{}).Run(context.Background(), []stage.Cmd{{Args: []string{filepath.Join(t.TempDir(), "missing")}}})
if err == nil || !strings.Contains(err.Error(), "failed to start command") {
t.Fatalf("start error = %v", err)
}
}
func TestRunClockTimeout(t *testing.T) {
results, err := (&Local{}).Run(context.Background(), []stage.Cmd{{
Args: []string{"/bin/sleep", "1"}, ClockLimit: uint64(20 * time.Millisecond),
}})
if err != nil {
t.Fatal(err)
}
if len(results) != 1 || results[0].Status != stage.StatusTimeLimitExceeded {
t.Fatalf("Run() results = %+v", results)
}
}
func TestRunRejectsEmptyArgs(t *testing.T) {
_, err := (&Local{}).Run(context.Background(), []stage.Cmd{{}})
if err == nil || err.Error() != "command args must not be empty" {

View File

@ -7,9 +7,57 @@ import (
"strings"
"testing"
"github.com/criyle/go-judge/pb"
"github.com/joint-online-judge/JOJ3/internal/stage"
)
func TestConvertPBFileVariants(t *testing.T) {
content, fileID, name := "content", "cached", "output"
max := int64(123)
tests := []struct {
name string
file stage.CmdFile
check func(*pb.Request_File) bool
}{
{"content", stage.CmdFile{Content: &content}, func(f *pb.Request_File) bool { return string(f.GetMemory().GetContent()) == content }},
{"cached", stage.CmdFile{FileID: &fileID}, func(f *pb.Request_File) bool { return f.GetCached().GetFileID() == fileID }},
{"pipe", stage.CmdFile{Name: &name, Max: &max, Pipe: true}, func(f *pb.Request_File) bool {
return f.GetPipe().GetName() == name && f.GetPipe().GetMax() == max && f.GetPipe().GetPipe()
}},
{"stream-in", stage.CmdFile{StreamIn: true}, func(f *pb.Request_File) bool { return f.GetStreamIn() != nil }},
{"stream-out", stage.CmdFile{StreamOut: true}, func(f *pb.Request_File) bool { return f.GetStreamOut() != nil }},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := convertPBFile(tt.file)
if err != nil || got == nil || !tt.check(got) {
t.Fatalf("convertPBFile() = %v, %v", got, err)
}
})
}
}
func TestConvertCopyOutAndResult(t *testing.T) {
copyOut := convertPBCopyOut([]string{"required", "optional?"})
if len(copyOut) != 2 || copyOut[0].GetOptional() || !copyOut[1].GetOptional() || copyOut[1].GetName() != "optional" {
t.Fatalf("convertPBCopyOut() = %+v", copyOut)
}
fileError := &pb.Response_FileError{}
fileError.SetName("input")
fileError.SetType(pb.Response_FileError_ErrorType(1))
fileError.SetMessage("bad file")
response := &pb.Response_Result{}
response.SetStatus(pb.Response_Result_StatusType(stage.StatusAccepted))
response.SetFiles(map[string][]byte{"stdout": []byte("ok")})
response.SetFileIDs(map[string]string{"bin": "id"})
response.SetFileError([]*pb.Response_FileError{fileError})
got := convertPBResult([]*pb.Response_Result{response})
if len(got) != 1 || got[0].Files["stdout"] != "ok" || got[0].FileIDs["bin"] != "id" ||
len(got[0].FileError) != 1 || got[0].FileError[0].Message != "bad file" {
t.Fatalf("convertPBResult() = %+v", got)
}
}
func TestConvertPBCmdReturnsSourceReadError(t *testing.T) {
missing := t.TempDir() + "/missing"
_, err := convertPBCmd([]stage.Cmd{{
@ -46,8 +94,10 @@ func TestConvertPBCmdReturnsCopyInDirectoryWalkError(t *testing.T) {
}
}
func TestConvertPBCmdPreservesMoreThanSCMMaxFDFileCount(t *testing.T) {
const fileCount = 301
func TestConvertPBCmdPreservesFilesAcrossMultipleSCMMaxFDBatches(t *testing.T) {
// 1001 is large enough to require several descriptor batches while keeping
// the fixture cheap to create and the protobuf request small.
const fileCount = 1001
dir := t.TempDir()
for i := range fileCount {
path := filepath.Join(dir, fmt.Sprintf("file-%03d", i))

View File

@ -0,0 +1,27 @@
package sandbox
import (
"context"
"testing"
)
func TestTokenAuth(t *testing.T) {
auth := newTokenAuth("secret")
metadata, err := auth.GetRequestMetadata(context.Background())
if err != nil || metadata["authorization"] != "Bearer secret" {
t.Fatalf("GetRequestMetadata() = %v, %v", metadata, err)
}
if auth.RequireTransportSecurity() {
t.Fatal("RequireTransportSecurity() = true")
}
}
func TestCreateGRPCConnection(t *testing.T) {
conn, err := createGRPCConnection("passthrough:///unused", "secret")
if err != nil {
t.Fatal(err)
}
if err := conn.Close(); err != nil {
t.Fatal(err)
}
}

View File

@ -0,0 +1,38 @@
package keyword
import (
"strings"
"testing"
"github.com/joint-online-judge/JOJ3/internal/stage"
)
func TestRunCountsCapsAndOrdersKeywords(t *testing.T) {
results, forceQuit, err := (&Keyword{}).Run([]stage.ExecutorResult{{
Files: map[string]string{"log": "error error warning"},
}}, map[string]any{
"score": 10,
"files": []any{"log"},
"forceQuitOnDeduct": true,
"matches": []any{
map[string]any{"keywords": []any{"error"}, "score": 3, "maxMatchCount": 1},
map[string]any{"keywords": []any{"warning"}, "score": 2},
},
})
if err != nil {
t.Fatal(err)
}
if len(results) != 1 || results[0].Score != 5 || !forceQuit {
t.Fatalf("Run() = %+v, %v", results, forceQuit)
}
if !strings.Contains(results[0].Comment, "`error`: 1") || !strings.Contains(results[0].Comment, "`warning`: 1") {
t.Fatalf("comment = %q", results[0].Comment)
}
}
func TestRunRejectsInvalidConfiguration(t *testing.T) {
_, forceQuit, err := (&Keyword{}).Run(nil, "invalid")
if err == nil || !forceQuit {
t.Fatalf("Run() = forceQuit %v, error %v", forceQuit, err)
}
}

View File

@ -60,7 +60,7 @@ func (t FileErrorType) MarshalJSON() ([]byte, error) {
func (t *FileErrorType) UnmarshalJSON(b []byte) error {
str := string(b)
v, ok := fileErrorStringReverse[str]
if ok {
if !ok {
return fmt.Errorf("%s is not file error type", str)
}
*t = v

View File

@ -0,0 +1,67 @@
package stage
import (
"encoding/json"
"strings"
"testing"
)
func TestStatusJSONRoundTrip(t *testing.T) {
for status := StatusInvalid; status <= StatusInternalError; status++ {
data, err := json.Marshal(status)
if err != nil {
t.Fatal(err)
}
var got Status
if err := json.Unmarshal(data, &got); err != nil {
t.Fatalf("unmarshal %s: %v", data, err)
}
if got != status {
t.Fatalf("round trip = %v, want %v", got, status)
}
}
var status Status
if err := json.Unmarshal([]byte(`"unknown"`), &status); err == nil {
t.Fatal("unknown status was accepted")
}
}
func TestFileErrorTypeJSONRoundTrip(t *testing.T) {
for fileError := ErrCopyInOpenFile; fileError <= ErrCollectSizeExceeded; fileError++ {
data, err := json.Marshal(fileError)
if err != nil {
t.Fatal(err)
}
var got FileErrorType
if err := json.Unmarshal(data, &got); err != nil {
t.Fatalf("unmarshal %s: %v", data, err)
}
if got != fileError {
t.Fatalf("round trip = %v, want %v", got, fileError)
}
}
var fileError FileErrorType
if err := json.Unmarshal([]byte(`"unknown"`), &fileError); err == nil {
t.Fatal("unknown file error type was accepted")
}
}
func TestExecutorResultJSONSummarizesFileContents(t *testing.T) {
data, err := json.Marshal(ExecutorResult{
Status: StatusAccepted,
Files: map[string]string{"stdout": "secret output"},
})
if err != nil {
t.Fatal(err)
}
if strings.Contains(string(data), "secret output") || !strings.Contains(string(data), `"stdout":"len:13"`) {
t.Fatalf("MarshalJSON() = %s", data)
}
}
func TestNonNullSliceMarshalsEmptyAsArray(t *testing.T) {
data, err := json.Marshal(NonNullSlice[int](nil))
if err != nil || string(data) != "[]" {
t.Fatalf("MarshalJSON() = %s, %v", data, err)
}
}

View File

@ -0,0 +1,80 @@
package healthcheck
import (
"os"
"path/filepath"
"strings"
"testing"
"time"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing/object"
)
func newCommitRepo(t *testing.T, message, email string) string {
t.Helper()
dir := t.TempDir()
repo, err := git.PlainInit(dir, false)
if err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(dir, "README.md"), []byte("test"), 0o600); err != nil {
t.Fatal(err)
}
worktree, err := repo.Worktree()
if err != nil {
t.Fatal(err)
}
if _, err := worktree.Add("README.md"); err != nil {
t.Fatal(err)
}
signature := &object.Signature{Name: "Student", Email: email, When: time.Unix(1, 0)}
if _, err := worktree.Commit(message, &git.CommitOptions{Author: signature, Committer: signature}); err != nil {
t.Fatal(err)
}
return dir
}
func TestCommitChecks(t *testing.T) {
valid := newCommitRepo(t, "feat: valid", "student@example.edu")
if err := NonASCIIMsg(valid); err != nil {
t.Fatal(err)
}
if err := AuthorEmailCheck(valid, []string{"example.edu"}, filepath.Join(valid, "missing.csv")); err != nil {
t.Fatal(err)
}
invalidMessage := newCommitRepo(t, "feat: 测试", "student@invalid.test")
if err := NonASCIIMsg(invalidMessage); err == nil || !strings.Contains(err.Error(), "测试") {
t.Fatalf("NonASCIIMsg() error = %v", err)
}
if err := AuthorEmailCheck(invalidMessage, []string{"example.edu"}, filepath.Join(invalidMessage, "missing.csv")); err == nil || !strings.Contains(err.Error(), "allowed domains") {
t.Fatalf("AuthorEmailCheck(domain) error = %v", err)
}
}
func TestAuthorEmailCheckActorCSV(t *testing.T) {
repo := newCommitRepo(t, "feat: valid", "student@example.edu")
csvPath := filepath.Join(t.TempDir(), "actors.csv")
if err := os.WriteFile(csvPath, []byte("name,id,student\n"), 0o600); err != nil {
t.Fatal(err)
}
if err := AuthorEmailCheck(repo, []string{"example.edu"}, csvPath); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(csvPath, []byte("name,id,other\n"), 0o600); err != nil {
t.Fatal(err)
}
if err := AuthorEmailCheck(repo, []string{"example.edu"}, csvPath); err == nil || !strings.Contains(err.Error(), "not stored") {
t.Fatalf("AuthorEmailCheck(actor) error = %v", err)
}
}
func TestCommitChecksRejectNonRepository(t *testing.T) {
if err := NonASCIIMsg(t.TempDir()); err == nil || !strings.Contains(err.Error(), "opening git repo") {
t.Fatalf("NonASCIIMsg() error = %v", err)
}
if err := AuthorEmailCheck(t.TempDir(), nil, ""); err == nil || !strings.Contains(err.Error(), "opening git repo") {
t.Fatalf("AuthorEmailCheck() error = %v", err)
}
}

View File

@ -0,0 +1,67 @@
package healthcheck
import (
"crypto/sha256"
"encoding/hex"
"os"
"path/filepath"
"strings"
"testing"
)
func TestParseWhitelistedChars(t *testing.T) {
got := parseWhitelistedChars("你, 好, a, invalid, ,你")
if len(got) != 2 {
t.Fatalf("parseWhitelistedChars() = %v", got)
}
if _, ok := got['你']; !ok {
t.Fatal("missing whitelisted rune")
}
}
func TestNonASCIIFiles(t *testing.T) {
dir := t.TempDir()
if err := os.WriteFile(filepath.Join(dir, "source.txt"), []byte("hello 你\n"), 0o600); err != nil {
t.Fatal(err)
}
if err := NonASCIIFiles(dir, "你"); err != nil {
t.Fatalf("whitelisted NonASCIIFiles() error = %v", err)
}
if err := NonASCIIFiles(dir, ""); err == nil || !strings.Contains(err.Error(), "source.txt") {
t.Fatalf("NonASCIIFiles() error = %v", err)
}
}
func TestForbiddenCheck(t *testing.T) {
dir := t.TempDir()
t.Chdir(dir)
if err := os.WriteFile(filepath.Join(dir, ".gitignore"), []byte("*.out\n"), 0o600); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(dir, "result.out"), nil, 0o600); err != nil {
t.Fatal(err)
}
if err := ForbiddenCheck("."); err == nil || !strings.Contains(err.Error(), "result.out") {
t.Fatalf("ForbiddenCheck() error = %v", err)
}
}
func TestVerifyFiles(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "protected.txt")
content := []byte("original")
if err := os.WriteFile(path, content, 0o600); err != nil {
t.Fatal(err)
}
sum := sha256.Sum256(content)
checksum := hex.EncodeToString(sum[:])
if err := VerifyFiles(dir, "protected.txt", checksum); err != nil {
t.Fatal(err)
}
if err := VerifyFiles(dir, "protected.txt", "bad"); err == nil || !strings.Contains(err.Error(), "altered") {
t.Fatalf("VerifyFiles(altered) error = %v", err)
}
if err := VerifyFiles(dir, "one,two", checksum); err == nil || !strings.Contains(err.Error(), "do not match") {
t.Fatalf("VerifyFiles(mismatch) error = %v", err)
}
}