713 lines
19 KiB
Go
713 lines
19 KiB
Go
// Copyright 2026 Alibaba Group
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package cli
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// writeTestFile is a test helper that writes data to a file and fails the
|
|
// test immediately if the write fails, preventing confusing downstream errors.
|
|
func writeTestFile(t *testing.T, path string, data []byte) {
|
|
t.Helper()
|
|
if err := os.WriteFile(path, data, 0o644); err != nil {
|
|
t.Fatalf("failed to write test file %s: %v", path, err)
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// ReadFileArg
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func TestReadFileArgPlainValue(t *testing.T) {
|
|
t.Parallel()
|
|
val, isFile, err := ReadFileArg("hello world")
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if isFile {
|
|
t.Error("plain value should not be detected as file")
|
|
}
|
|
if val != "hello world" {
|
|
t.Errorf("got %q, want %q", val, "hello world")
|
|
}
|
|
}
|
|
|
|
// TestReadFileArgChineseAtMention guards the @所有人-style mentions that the
|
|
// chat bot Webhook tests rely on: an '@' followed by non-ASCII text must be
|
|
// treated as a literal message, not as the @file injection syntax.
|
|
func TestReadFileArgChineseAtMention(t *testing.T) {
|
|
t.Parallel()
|
|
cases := []string{
|
|
"@所有人 这是 @ 所有人 的消息",
|
|
"@张三",
|
|
"@A 但接下来都是中文@测试",
|
|
}
|
|
for _, in := range cases {
|
|
val, isFile, err := ReadFileArg(in)
|
|
if in == "@A 但接下来都是中文@测试" {
|
|
// '@A' starts with ASCII, treated as path → expect file error
|
|
if err == nil {
|
|
t.Errorf("@A... should attempt file lookup; got val=%q isFile=%v", val, isFile)
|
|
}
|
|
continue
|
|
}
|
|
if err != nil {
|
|
t.Errorf("%q: unexpected error %v", in, err)
|
|
continue
|
|
}
|
|
if isFile {
|
|
t.Errorf("%q: should be plain text, got isFile=true", in)
|
|
}
|
|
if val != in {
|
|
t.Errorf("%q: got %q", in, val)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestReadFileArgReadsFile(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, "input.txt")
|
|
writeTestFile(t, path, []byte(`{"title":"test"}`))
|
|
|
|
val, isFile, err := ReadFileArg("@" + path)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if !isFile {
|
|
t.Error("@file value should be detected as file")
|
|
}
|
|
if val != `{"title":"test"}` {
|
|
t.Errorf("got %q, want %q", val, `{"title":"test"}`)
|
|
}
|
|
}
|
|
|
|
func TestReadFileArgEmptyFilename(t *testing.T) {
|
|
t.Parallel()
|
|
_, _, err := ReadFileArg("@")
|
|
if err == nil {
|
|
t.Fatal("expected error for empty filename")
|
|
}
|
|
if !strings.Contains(err.Error(), "must not be empty") {
|
|
t.Errorf("unexpected error: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestReadFileArgMissingFile(t *testing.T) {
|
|
t.Parallel()
|
|
_, _, err := ReadFileArg("@/nonexistent/path/file.txt")
|
|
if err == nil {
|
|
t.Fatal("expected error for missing file")
|
|
}
|
|
}
|
|
|
|
func TestReadFileArgSizeLimit(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, "huge.txt")
|
|
// Create a file slightly over maxStdinSize (write 10MB + 1 byte)
|
|
f, err := os.Create(path)
|
|
if err != nil {
|
|
t.Fatalf("failed to create test file: %v", err)
|
|
}
|
|
data := strings.Repeat("x", maxStdinSize+1)
|
|
if _, err := f.WriteString(data); err != nil {
|
|
f.Close()
|
|
t.Fatalf("failed to write test data: %v", err)
|
|
}
|
|
f.Close()
|
|
|
|
_, _, err = ReadFileArg("@" + path)
|
|
if err == nil {
|
|
t.Fatal("expected error for oversized file")
|
|
}
|
|
if !strings.Contains(err.Error(), "10 MB") {
|
|
t.Errorf("unexpected error: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestReadFileArgAtDashPassThrough(t *testing.T) {
|
|
t.Parallel()
|
|
// @- means stdin — ReadFileArg should NOT handle it, just pass through.
|
|
val, isFile, err := ReadFileArg("@-")
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if isFile {
|
|
t.Error("@- should not be treated as a file by ReadFileArg")
|
|
}
|
|
if val != "@-" {
|
|
t.Errorf("got %q, want %q", val, "@-")
|
|
}
|
|
}
|
|
|
|
func TestReadStdinIfPipedReturnsEmptyForTerminal(t *testing.T) {
|
|
// This test runs in a terminal context (go test), so stdin is a terminal.
|
|
// ReadStdinIfPiped should return empty string.
|
|
val, err := ReadStdinIfPiped()
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if val != "" {
|
|
t.Errorf("expected empty string for terminal stdin, got %q", val)
|
|
}
|
|
}
|
|
|
|
func TestStdinIsPipeReturnsFalseForTerminal(t *testing.T) {
|
|
// go test runs with stdin as a terminal, so StdinIsPipe should return false.
|
|
if StdinIsPipe() {
|
|
t.Error("expected StdinIsPipe() == false in terminal context")
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// readFileBounded (via ReadFileArg)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func TestReadFileBoundedEmptyFile(t *testing.T) {
|
|
t.Parallel()
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, "empty.txt")
|
|
writeTestFile(t, path, []byte(""))
|
|
|
|
val, isFile, err := ReadFileArg("@" + path)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if !isFile {
|
|
t.Error("should be detected as file")
|
|
}
|
|
if val != "" {
|
|
t.Errorf("expected empty content, got %q", val)
|
|
}
|
|
}
|
|
|
|
func TestReadFileBoundedRejectsDirectory(t *testing.T) {
|
|
t.Parallel()
|
|
dir := t.TempDir()
|
|
|
|
_, _, err := ReadFileArg("@" + dir)
|
|
if err == nil {
|
|
t.Fatal("expected error when reading a directory as a file")
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// StdinGuard
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func TestStdinGuardFirstClaimSucceeds(t *testing.T) {
|
|
t.Parallel()
|
|
guard := NewStdinGuard()
|
|
if err := guard.Claim("--text @-"); err != nil {
|
|
t.Fatalf("first claim should succeed: %v", err)
|
|
}
|
|
if !guard.Claimed() {
|
|
t.Error("guard should report claimed after successful claim")
|
|
}
|
|
}
|
|
|
|
func TestStdinGuardSecondClaimFails(t *testing.T) {
|
|
t.Parallel()
|
|
guard := NewStdinGuard()
|
|
_ = guard.Claim("--text @-")
|
|
|
|
err := guard.Claim("--body @-")
|
|
if err == nil {
|
|
t.Fatal("second claim should fail")
|
|
}
|
|
if !strings.Contains(err.Error(), "--text @-") {
|
|
t.Errorf("error should mention first claimer, got: %v", err)
|
|
}
|
|
if !strings.Contains(err.Error(), "--body @-") {
|
|
t.Errorf("error should mention second claimer, got: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestStdinGuardNotClaimedInitially(t *testing.T) {
|
|
t.Parallel()
|
|
guard := NewStdinGuard()
|
|
if guard.Claimed() {
|
|
t.Error("fresh guard should not be claimed")
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// ResolveInputSource
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func TestResolveInputSourcePlainValue(t *testing.T) {
|
|
t.Parallel()
|
|
guard := NewStdinGuard()
|
|
val, err := ResolveInputSource("hello", "text", guard)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if val != "hello" {
|
|
t.Errorf("got %q, want %q", val, "hello")
|
|
}
|
|
if guard.Claimed() {
|
|
t.Error("plain value should not claim stdin")
|
|
}
|
|
}
|
|
|
|
func TestResolveInputSourceEmptyValue(t *testing.T) {
|
|
t.Parallel()
|
|
guard := NewStdinGuard()
|
|
val, err := ResolveInputSource("", "text", guard)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if val != "" {
|
|
t.Errorf("got %q, want empty", val)
|
|
}
|
|
}
|
|
|
|
func TestResolveInputSourceAtFileReadsFile(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, "msg.txt")
|
|
writeTestFile(t, path, []byte("file content here"))
|
|
|
|
guard := NewStdinGuard()
|
|
val, err := ResolveInputSource("@"+path, "text", guard)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if val != "file content here" {
|
|
t.Errorf("got %q, want %q", val, "file content here")
|
|
}
|
|
// @file should NOT claim stdin.
|
|
if guard.Claimed() {
|
|
t.Error("@file should not claim stdin")
|
|
}
|
|
}
|
|
|
|
func TestResolveInputSourceAtFileEmptyName(t *testing.T) {
|
|
t.Parallel()
|
|
guard := NewStdinGuard()
|
|
_, err := ResolveInputSource("@", "json", guard)
|
|
if err == nil {
|
|
t.Fatal("expected error for empty @file name")
|
|
}
|
|
if !strings.Contains(err.Error(), "--json") {
|
|
t.Errorf("error should mention flag name, got: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestResolveInputSourceAtFileMissing(t *testing.T) {
|
|
t.Parallel()
|
|
guard := NewStdinGuard()
|
|
_, err := ResolveInputSource("@/no/such/file.txt", "data", guard)
|
|
if err == nil {
|
|
t.Fatal("expected error for missing file")
|
|
}
|
|
if !strings.Contains(err.Error(), "--data") {
|
|
t.Errorf("error should mention flag name, got: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestResolveInputSourceAtFileSizeLimit(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, "huge.bin")
|
|
f, err := os.Create(path)
|
|
if err != nil {
|
|
t.Fatalf("failed to create test file: %v", err)
|
|
}
|
|
if _, err := f.WriteString(strings.Repeat("x", maxStdinSize+1)); err != nil {
|
|
f.Close()
|
|
t.Fatalf("failed to write test data: %v", err)
|
|
}
|
|
f.Close()
|
|
|
|
guard := NewStdinGuard()
|
|
_, resolveErr := ResolveInputSource("@"+path, "body", guard)
|
|
if resolveErr == nil {
|
|
t.Fatal("expected error for oversized file")
|
|
}
|
|
if !strings.Contains(resolveErr.Error(), "10 MB") {
|
|
t.Errorf("error should mention size limit, got: %v", resolveErr)
|
|
}
|
|
}
|
|
|
|
func TestResolveInputSourceAtDashNilGuardFails(t *testing.T) {
|
|
t.Parallel()
|
|
_, err := ResolveInputSource("@-", "text", nil)
|
|
if err == nil {
|
|
t.Fatal("expected error when guard is nil")
|
|
}
|
|
if !strings.Contains(err.Error(), "not available") {
|
|
t.Errorf("error should mention stdin unavailability, got: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestResolveInputSourceAtDashDoubleClaimFails(t *testing.T) {
|
|
t.Parallel()
|
|
guard := NewStdinGuard()
|
|
// Simulate first claim from another source.
|
|
_ = guard.Claim("--json @-")
|
|
|
|
_, err := ResolveInputSource("@-", "text", guard)
|
|
if err == nil {
|
|
t.Fatal("expected error for double stdin claim")
|
|
}
|
|
if !strings.Contains(err.Error(), "already consumed") {
|
|
t.Errorf("error should mention stdin conflict, got: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestResolveInputSourceAtFileMultiline(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, "multiline.md")
|
|
content := "# Title\n\nLine 1\nLine 2\nLine 3\n"
|
|
writeTestFile(t, path, []byte(content))
|
|
|
|
guard := NewStdinGuard()
|
|
val, err := ResolveInputSource("@"+path, "text", guard)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if val != content {
|
|
t.Errorf("multiline content mismatch:\ngot: %q\nwant: %q", val, content)
|
|
}
|
|
}
|
|
|
|
func TestResolveInputSourceAtFileUTF8(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, "chinese.txt")
|
|
content := "你好世界 🌍"
|
|
writeTestFile(t, path, []byte(content))
|
|
|
|
guard := NewStdinGuard()
|
|
val, err := ResolveInputSource("@"+path, "text", guard)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if val != content {
|
|
t.Errorf("UTF-8 content mismatch:\ngot: %q\nwant: %q", val, content)
|
|
}
|
|
}
|
|
|
|
func TestResolveInputSourceAtFileJSON(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, "payload.json")
|
|
content := `{"title":"meeting","startTime":"2026-03-29T10:00:00Z"}`
|
|
writeTestFile(t, path, []byte(content))
|
|
|
|
guard := NewStdinGuard()
|
|
val, err := ResolveInputSource("@"+path, "json", guard)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if val != content {
|
|
t.Errorf("JSON content mismatch:\ngot: %q\nwant: %q", val, content)
|
|
}
|
|
}
|
|
|
|
func TestResolveInputSourceValueStartingWithAtSign(t *testing.T) {
|
|
t.Parallel()
|
|
// A value like "@mention" that looks like @file but the file doesn't exist
|
|
// should return an error (user likely intended file input).
|
|
guard := NewStdinGuard()
|
|
_, err := ResolveInputSource("@mention_someone", "text", guard)
|
|
if err == nil {
|
|
t.Fatal("expected error for non-existent @file path")
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// ResolveInputSource: table-driven edge cases
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func TestResolveInputSourceTable(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
dir := t.TempDir()
|
|
existingFile := filepath.Join(dir, "exists.txt")
|
|
writeTestFile(t, existingFile, []byte("file-data"))
|
|
|
|
tests := []struct {
|
|
name string
|
|
value string
|
|
flagName string
|
|
guard *StdinGuard
|
|
wantVal string
|
|
wantErr string // substring to match in error, "" means no error
|
|
wantClaim bool // expect guard to be claimed after call
|
|
}{
|
|
{
|
|
name: "plain string unchanged",
|
|
value: "hello",
|
|
flagName: "text",
|
|
guard: NewStdinGuard(),
|
|
wantVal: "hello",
|
|
},
|
|
{
|
|
name: "empty string unchanged",
|
|
value: "",
|
|
flagName: "text",
|
|
guard: NewStdinGuard(),
|
|
wantVal: "",
|
|
},
|
|
{
|
|
name: "plain string with special chars",
|
|
value: "hello@world.com",
|
|
flagName: "email",
|
|
guard: NewStdinGuard(),
|
|
wantVal: "hello@world.com",
|
|
},
|
|
{
|
|
name: "@file reads content",
|
|
value: "@" + existingFile,
|
|
flagName: "data",
|
|
guard: NewStdinGuard(),
|
|
wantVal: "file-data",
|
|
},
|
|
{
|
|
name: "@file does not claim stdin",
|
|
value: "@" + existingFile,
|
|
flagName: "data",
|
|
guard: NewStdinGuard(),
|
|
wantVal: "file-data",
|
|
wantClaim: false,
|
|
},
|
|
{
|
|
name: "bare @ is error",
|
|
value: "@",
|
|
flagName: "body",
|
|
guard: NewStdinGuard(),
|
|
wantErr: "must not be empty",
|
|
},
|
|
{
|
|
name: "missing file is error",
|
|
value: "@/tmp/does-not-exist-" + t.Name(),
|
|
flagName: "file",
|
|
guard: NewStdinGuard(),
|
|
wantErr: "--file",
|
|
},
|
|
{
|
|
name: "@- with nil guard is error",
|
|
value: "@-",
|
|
flagName: "text",
|
|
guard: nil,
|
|
wantErr: "not available",
|
|
},
|
|
{
|
|
name: "@- with pre-claimed guard is error",
|
|
value: "@-",
|
|
flagName: "body",
|
|
guard: func() *StdinGuard {
|
|
g := NewStdinGuard()
|
|
_ = g.Claim("--json @-")
|
|
return g
|
|
}(),
|
|
wantErr: "already consumed",
|
|
},
|
|
{
|
|
name: "error message includes flag name",
|
|
value: "@/nonexistent",
|
|
flagName: "my-flag",
|
|
guard: NewStdinGuard(),
|
|
wantErr: "--my-flag",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
val, err := ResolveInputSource(tt.value, tt.flagName, tt.guard)
|
|
if tt.wantErr != "" {
|
|
if err == nil {
|
|
t.Fatalf("expected error containing %q, got nil", tt.wantErr)
|
|
}
|
|
if !strings.Contains(err.Error(), tt.wantErr) {
|
|
t.Errorf("error = %q, want substring %q", err.Error(), tt.wantErr)
|
|
}
|
|
return
|
|
}
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if val != tt.wantVal {
|
|
t.Errorf("value = %q, want %q", val, tt.wantVal)
|
|
}
|
|
if tt.guard != nil && tt.wantClaim != tt.guard.Claimed() {
|
|
t.Errorf("guard.Claimed() = %v, want %v", tt.guard.Claimed(), tt.wantClaim)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// ReadFileArg: table-driven edge cases
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func TestReadFileArgTable(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
dir := t.TempDir()
|
|
existingFile := filepath.Join(dir, "data.txt")
|
|
writeTestFile(t, existingFile, []byte("content"))
|
|
emptyFile := filepath.Join(dir, "empty.txt")
|
|
writeTestFile(t, emptyFile, []byte(""))
|
|
|
|
tests := []struct {
|
|
name string
|
|
value string
|
|
wantVal string
|
|
wantIsFile bool
|
|
wantErr string
|
|
}{
|
|
{"plain string", "hello", "hello", false, ""},
|
|
{"empty string", "", "", false, ""},
|
|
{"email-like value", "user@domain.com", "user@domain.com", false, ""},
|
|
{"@file reads content", "@" + existingFile, "content", true, ""},
|
|
{"@file empty content", "@" + emptyFile, "", true, ""},
|
|
{"@- passes through", "@-", "@-", false, ""},
|
|
{"bare @ is error", "@", "", false, "must not be empty"},
|
|
{"missing file is error", "@/nonexistent", "", false, "@file"},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
val, isFile, err := ReadFileArg(tt.value)
|
|
if tt.wantErr != "" {
|
|
if err == nil {
|
|
t.Fatalf("expected error containing %q, got nil", tt.wantErr)
|
|
}
|
|
if !strings.Contains(err.Error(), tt.wantErr) {
|
|
t.Errorf("error = %q, want substring %q", err.Error(), tt.wantErr)
|
|
}
|
|
return
|
|
}
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if val != tt.wantVal {
|
|
t.Errorf("value = %q, want %q", val, tt.wantVal)
|
|
}
|
|
if isFile != tt.wantIsFile {
|
|
t.Errorf("isFile = %v, want %v", isFile, tt.wantIsFile)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// StdinGuard: table-driven claim sequences
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func TestStdinGuardClaimSequences(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
name string
|
|
claims []string // sources to claim in order
|
|
wantFails int // how many claims should fail
|
|
wantErrSub string // substring expected in first failure
|
|
}{
|
|
{
|
|
name: "single claim succeeds",
|
|
claims: []string{"--json @-"},
|
|
wantFails: 0,
|
|
},
|
|
{
|
|
name: "second claim fails",
|
|
claims: []string{"--json @-", "--text @-"},
|
|
wantFails: 1,
|
|
wantErrSub: "already consumed",
|
|
},
|
|
{
|
|
name: "third claim also fails",
|
|
claims: []string{"--json @-", "--text @-", "--body @-"},
|
|
wantFails: 2,
|
|
wantErrSub: "already consumed",
|
|
},
|
|
{
|
|
name: "error names both sources",
|
|
claims: []string{"implicit stdin (pipe)", "--text @-"},
|
|
wantFails: 1,
|
|
wantErrSub: "implicit stdin (pipe)",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
guard := NewStdinGuard()
|
|
fails := 0
|
|
var firstErr error
|
|
for _, source := range tt.claims {
|
|
if err := guard.Claim(source); err != nil {
|
|
fails++
|
|
if firstErr == nil {
|
|
firstErr = err
|
|
}
|
|
}
|
|
}
|
|
if fails != tt.wantFails {
|
|
t.Errorf("failures = %d, want %d", fails, tt.wantFails)
|
|
}
|
|
if tt.wantErrSub != "" && firstErr != nil {
|
|
if !strings.Contains(firstErr.Error(), tt.wantErrSub) {
|
|
t.Errorf("first error = %q, want substring %q", firstErr.Error(), tt.wantErrSub)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// StdinGuard concurrency safety
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func TestStdinGuardConcurrentClaims(t *testing.T) {
|
|
t.Parallel()
|
|
guard := NewStdinGuard()
|
|
|
|
const goroutines = 50
|
|
results := make(chan error, goroutines)
|
|
for i := 0; i < goroutines; i++ {
|
|
go func(id int) {
|
|
results <- guard.Claim("goroutine")
|
|
}(i)
|
|
}
|
|
|
|
successCount := 0
|
|
for i := 0; i < goroutines; i++ {
|
|
if err := <-results; err == nil {
|
|
successCount++
|
|
}
|
|
}
|
|
if successCount != 1 {
|
|
t.Errorf("exactly one goroutine should succeed, got %d", successCount)
|
|
}
|
|
}
|