summaryrefslogtreecommitdiff
path: root/internal/lock/lock_test.go
blob: fca4d76aaa93d6ba934fe67f9ac61990aa2b0bbf (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
// SPDX-License-Identifier: GPL-3.0-or-later

package lock

import (
	"context"
	"errors"
	"fmt"
	"os"
	"path/filepath"
	"strings"
	"testing"
	"time"
)

func TestAcquireAndRelease(t *testing.T) {
	path := filepath.Join(t.TempDir(), "state", "dl.lock")
	l, err := Acquire(context.Background(), path, false)
	if err != nil {
		t.Fatal(err)
	}
	if _, err := os.Stat(path); err != nil {
		t.Errorf("lock file missing: %v", err)
	}
	if b, _ := os.ReadFile(path); !strings.Contains(string(b), fmt.Sprint(os.Getpid())) {
		t.Errorf("lock file does not name the holder's pid: %q", b)
	}
	if err := l.Release(); err != nil {
		t.Fatal(err)
	}
	if _, err := os.Stat(path); !os.IsNotExist(err) {
		t.Error("Release left the lock file behind")
	}
	if err := l.Release(); err != nil {
		t.Errorf("a second Release must be harmless: %v", err)
	}
}

func TestAcquireFailsWhenHeldAndNotWaiting(t *testing.T) {
	path := filepath.Join(t.TempDir(), "dl.lock")
	first, err := Acquire(context.Background(), path, false)
	if err != nil {
		t.Fatal(err)
	}
	defer first.Release()
	if _, err := Acquire(context.Background(), path, false); !errors.Is(err, ErrHeld) {
		t.Fatalf("second Acquire err = %v, want ErrHeld", err)
	}
}

func TestAcquireWaitsUntilReleased(t *testing.T) {
	path := filepath.Join(t.TempDir(), "dl.lock")
	first, err := Acquire(context.Background(), path, false)
	if err != nil {
		t.Fatal(err)
	}
	go func() {
		time.Sleep(150 * time.Millisecond)
		first.Release()
	}()
	start := time.Now()
	second, err := Acquire(context.Background(), path, true)
	if err != nil {
		t.Fatalf("waiting Acquire failed: %v", err)
	}
	defer second.Release()
	if time.Since(start) < 100*time.Millisecond {
		t.Error("Acquire returned before the first holder released")
	}
}

// TestAcquireRespectsContextCancellation is fix round 2026-09-12/item 3: a
// waiting Acquire must not ignore an interrupt - a cancelled ctx must return
// promptly with ctx.Err(), not poll forever. The unfixed code HANGS rather
// than fails here, so the wait for Acquire's result is itself bounded with
// its own hard timeout: a regression must fail this test, not hang the
// whole suite.
func TestAcquireRespectsContextCancellation(t *testing.T) {
	path := filepath.Join(t.TempDir(), "dl.lock")
	held, err := Acquire(context.Background(), path, false)
	if err != nil {
		t.Fatal(err)
	}
	defer held.Release()

	ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
	defer cancel()

	result := make(chan error, 1)
	start := time.Now()
	go func() {
		_, err := Acquire(ctx, path, true)
		result <- err
	}()

	select {
	case err := <-result:
		if !errors.Is(err, context.DeadlineExceeded) {
			t.Fatalf("Acquire err = %v, want context.DeadlineExceeded", err)
		}
		if elapsed := time.Since(start); elapsed > 500*time.Millisecond {
			t.Errorf("Acquire took %v to notice cancellation, want well under a second", elapsed)
		}
	case <-time.After(2 * time.Second):
		t.Fatal("Acquire ignored context cancellation and is still blocked")
	}
}

// TestStaleLockIsTakenOver: a lock naming a pid that is not running must not
// wedge krino - a machine that lost power mid-run would need manual cleanup.
func TestStaleLockIsTakenOver(t *testing.T) {
	path := filepath.Join(t.TempDir(), "dl.lock")
	if err := os.WriteFile(path, []byte("pid 4294967000\nstarted 2020-01-01T00:00:00Z\n"), 0o644); err != nil {
		t.Fatal(err)
	}
	l, err := Acquire(context.Background(), path, false)
	if err != nil {
		t.Fatalf("a stale lock blocked Acquire: %v", err)
	}
	defer l.Release()
	if !l.TookOverStale {
		t.Error("the takeover was not reported to the caller")
	}
}