// Copyright 2018 The gVisor Authors.
//
// 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 lock

import (
	"reflect"
	"testing"
)

type entry struct {
	Lock
	LockRange
}

func equals(e0, e1 []entry) bool {
	if len(e0) != len(e1) {
		return false
	}
	for i := range e0 {
		for k := range e0[i].Lock.Readers {
			if !e1[i].Lock.Readers[k] {
				return false
			}
		}
		for k := range e1[i].Lock.Readers {
			if !e0[i].Lock.Readers[k] {
				return false
			}
		}
		if !reflect.DeepEqual(e0[i].LockRange, e1[i].LockRange) {
			return false
		}
		if e0[i].Lock.Writer != e1[i].Lock.Writer {
			return false
		}
	}
	return true
}

// fill a LockSet with consecutive region locks.  Will panic if
// LockRanges are not consecutive.
func fill(entries []entry) LockSet {
	l := LockSet{}
	for _, e := range entries {
		gap := l.FindGap(e.LockRange.Start)
		if !gap.Ok() {
			panic("cannot insert into existing segment")
		}
		l.Insert(gap, e.LockRange, e.Lock)
	}
	return l
}

func TestCanLockEmpty(t *testing.T) {
	l := LockSet{}

	// Expect to be able to take any locks given that the set is empty.
	eof := l.FirstGap().End()
	r := LockRange{0, eof}
	if !l.canLock(1, ReadLock, r) {
		t.Fatalf("canLock type %d for range %v and uid %d got false, want true", ReadLock, r, 1)
	}
	if !l.canLock(2, ReadLock, r) {
		t.Fatalf("canLock type %d for range %v and uid %d got false, want true", ReadLock, r, 2)
	}
	if !l.canLock(1, WriteLock, r) {
		t.Fatalf("canLock type %d for range %v and uid %d got false, want true", WriteLock, r, 1)
	}
	if !l.canLock(2, WriteLock, r) {
		t.Fatalf("canLock type %d for range %v and uid %d got false, want true", WriteLock, r, 2)
	}
}

func TestCanLock(t *testing.T) {
	// + -------------- + ---------- + -------------- + --------- +
	// | Readers 1 & 2  | Readers 1  | Readers 1 & 3  | Writer 1  |
	// + -------------  + ---------- + -------------- + --------- +
	// 0             1024         2048             3072        4096
	l := fill([]entry{
		{
			Lock:      Lock{Readers: map[UniqueID]bool{1: true, 2: true}},
			LockRange: LockRange{0, 1024},
		},
		{
			Lock:      Lock{Readers: map[UniqueID]bool{1: true}},
			LockRange: LockRange{1024, 2048},
		},
		{
			Lock:      Lock{Readers: map[UniqueID]bool{1: true, 3: true}},
			LockRange: LockRange{2048, 3072},
		},
		{
			Lock:      Lock{Writer: 1},
			LockRange: LockRange{3072, 4096},
		},
	})

	// Now that we have a mildly interesting layout, try some checks on different
	// ranges, uids, and lock types.
	//
	// Expect to be able to extend the read lock, despite the writer lock, because
	// the writer has the same uid as the requested read lock.
	r := LockRange{0, 8192}
	if !l.canLock(1, ReadLock, r) {
		t.Fatalf("canLock type %d for range %v and uid %d got false, want true", ReadLock, r, 1)
	}
	// Expect to *not* be able to extend the read lock since there is an overlapping
	// writer region locked by someone other than the uid.
	if l.canLock(2, ReadLock, r) {
		t.Fatalf("canLock type %d for range %v and uid %d got true, want false", ReadLock, r, 2)
	}
	// Expect to be able to extend the read lock if there are only other readers in
	// the way.
	r = LockRange{64, 3072}
	if !l.canLock(2, ReadLock, r) {
		t.Fatalf("canLock type %d for range %v and uid %d got false, want true", ReadLock, r, 2)
	}
	// Expect to be able to set a read lock beyond the range of any existing locks.
	r = LockRange{4096, 10240}
	if !l.canLock(2, ReadLock, r) {
		t.Fatalf("canLock type %d for range %v and uid %d got false, want true", ReadLock, r, 2)
	}

	// Expect to not be able to take a write lock with other readers in the way.
	r = LockRange{0, 8192}
	if l.canLock(1, WriteLock, r) {
		t.Fatalf("canLock type %d for range %v and uid %d got true, want false", WriteLock, r, 1)
	}
	// Expect to be able to extend the write lock for the same uid.
	r = LockRange{3072, 8192}
	if !l.canLock(1, WriteLock, r) {
		t.Fatalf("canLock type %d for range %v and uid %d got false, want true", WriteLock, r, 1)
	}
	// Expect to not be able to overlap a write lock for two different uids.
	if l.canLock(2, WriteLock, r) {
		t.Fatalf("canLock type %d for range %v and uid %d got true, want false", WriteLock, r, 2)
	}
	// Expect to be able to set a write lock that is beyond the range of any
	// existing locks.
	r = LockRange{8192, 10240}
	if !l.canLock(2, WriteLock, r) {
		t.Fatalf("canLock type %d for range %v and uid %d got false, want true", WriteLock, r, 2)
	}
	// Expect to be able to upgrade a read lock (any portion of it).
	r = LockRange{1024, 2048}
	if !l.canLock(1, WriteLock, r) {
		t.Fatalf("canLock type %d for range %v and uid %d got false, want true", WriteLock, r, 1)
	}
	r = LockRange{1080, 2000}
	if !l.canLock(1, WriteLock, r) {
		t.Fatalf("canLock type %d for range %v and uid %d got false, want true", WriteLock, r, 1)
	}
}

func TestSetLock(t *testing.T) {
	tests := []struct {
		// description of test.
		name string

		// LockSet entries to pre-fill.
		before []entry

		// Description of region to lock:
		//
		// start is the file offset of the lock.
		start uint64
		// end is the end file offset of the lock.
		end uint64
		// uid of lock attempter.
		uid UniqueID
		// lock type requested.
		lockType LockType

		// success is true if taking the above
		// lock should succeed.
		success bool

		// Expected layout of the set after locking
		// if success is true.
		after []entry
	}{
		{
			name:     "set zero length ReadLock on empty set",
			start:    0,
			end:      0,
			uid:      0,
			lockType: ReadLock,
			success:  true,
		},
		{
			name:     "set zero length WriteLock on empty set",
			start:    0,
			end:      0,
			uid:      0,
			lockType: WriteLock,
			success:  true,
		},
		{
			name:     "set ReadLock on empty set",
			start:    0,
			end:      LockEOF,
			uid:      0,
			lockType: ReadLock,
			success:  true,
			// + ----------------------------------------- +
			// | Readers 0                                 |
			// + ----------------------------------------- +
			// 0                                  max uint64
			after: []entry{
				{
					Lock:      Lock{Readers: map[UniqueID]bool{0: true}},
					LockRange: LockRange{0, LockEOF},
				},
			},
		},
		{
			name:     "set WriteLock on empty set",
			start:    0,
			end:      LockEOF,
			uid:      0,
			lockType: WriteLock,
			success:  true,
			// + ----------------------------------------- +
			// | Writer  0                                 |
			// + ----------------------------------------- +
			// 0                                  max uint64
			after: []entry{
				{
					Lock:      Lock{Writer: 0},
					LockRange: LockRange{0, LockEOF},
				},
			},
		},
		{
			name: "set ReadLock on WriteLock same uid",
			// + ----------------------------------------- +
			// | Writer 0                                  |
			// + ----------------------------------------- +
			// 0                                  max uint64
			before: []entry{
				{
					Lock:      Lock{Writer: 0},
					LockRange: LockRange{0, LockEOF},
				},
			},
			start:    0,
			end:      4096,
			uid:      0,
			lockType: ReadLock,
			success:  true,
			// + ----------- + --------------------------- +
			// | Readers 0   | Writer 0                    |
			// + ----------- + --------------------------- +
			// 0          4096                    max uint64
			after: []entry{
				{
					Lock:      Lock{Readers: map[UniqueID]bool{0: true}},
					LockRange: LockRange{0, 4096},
				},
				{
					Lock:      Lock{Writer: 0},
					LockRange: LockRange{4096, LockEOF},
				},
			},
		},
		{
			name: "set WriteLock on ReadLock same uid",
			// + ----------------------------------------- +
			// | Readers 0                                 |
			// + ----------------------------------------- +
			// 0                                  max uint64
			before: []entry{
				{
					Lock:      Lock{Readers: map[UniqueID]bool{0: true}},
					LockRange: LockRange{0, LockEOF},
				},
			},
			start:    0,
			end:      4096,
			uid:      0,
			lockType: WriteLock,
			success:  true,
			// + ----------- + --------------------------- +
			// | Writer 0    | Readers 0                   |
			// + ----------- + --------------------------- +
			// 0          4096                    max uint64
			after: []entry{
				{
					Lock:      Lock{Writer: 0},
					LockRange: LockRange{0, 4096},
				},
				{
					Lock:      Lock{Readers: map[UniqueID]bool{0: true}},
					LockRange: LockRange{4096, LockEOF},
				},
			},
		},
		{
			name: "set ReadLock on WriteLock different uid",
			// + ----------------------------------------- +
			// | Writer 0                                  |
			// + ----------------------------------------- +
			// 0                                  max uint64
			before: []entry{
				{
					Lock:      Lock{Writer: 0},
					LockRange: LockRange{0, LockEOF},
				},
			},
			start:    0,
			end:      4096,
			uid:      1,
			lockType: ReadLock,
			success:  false,
		},
		{
			name: "set WriteLock on ReadLock different uid",
			// + ----------------------------------------- +
			// | Readers 0                                 |
			// + ----------------------------------------- +
			// 0                                  max uint64
			before: []entry{
				{
					Lock:      Lock{Readers: map[UniqueID]bool{0: true}},
					LockRange: LockRange{0, LockEOF},
				},
			},
			start:    0,
			end:      4096,
			uid:      1,
			lockType: WriteLock,
			success:  false,
		},
		{
			name: "split ReadLock for overlapping lock at start 0",
			// + ----------------------------------------- +
			// | Readers 0                                 |
			// + ----------------------------------------- +
			// 0                                  max uint64
			before: []entry{
				{
					Lock:      Lock{Readers: map[UniqueID]bool{0: true}},
					LockRange: LockRange{0, LockEOF},
				},
			},
			start:    0,
			end:      4096,
			uid:      1,
			lockType: ReadLock,
			success:  true,
			// + -------------- + --------------------------- +
			// | Readers 0 & 1  | Readers 0                   |
			// + -------------- + --------------------------- +
			// 0             4096                    max uint64
			after: []entry{
				{
					Lock:      Lock{Readers: map[UniqueID]bool{0: true, 1: true}},
					LockRange: LockRange{0, 4096},
				},
				{
					Lock:      Lock{Readers: map[UniqueID]bool{0: true}},
					LockRange: LockRange{4096, LockEOF},
				},
			},
		},
		{
			name: "split ReadLock for overlapping lock at non-zero start",
			// + ----------------------------------------- +
			// | Readers 0                                 |
			// + ----------------------------------------- +
			// 0                                  max uint64
			before: []entry{
				{
					Lock:      Lock{Readers: map[UniqueID]bool{0: true}},
					LockRange: LockRange{0, LockEOF},
				},
			},
			start:    4096,
			end:      8192,
			uid:      1,
			lockType: ReadLock,
			success:  true,
			// + ---------- + -------------- + ----------- +
			// | Readers 0  | Readers 0 & 1  | Readers 0   |
			// + ---------- + -------------- + ----------- +
			// 0         4096             8192    max uint64
			after: []entry{
				{
					Lock:      Lock{Readers: map[UniqueID]bool{0: true}},
					LockRange: LockRange{0, 4096},
				},
				{
					Lock:      Lock{Readers: map[UniqueID]bool{0: true, 1: true}},
					LockRange: LockRange{4096, 8192},
				},
				{
					Lock:      Lock{Readers: map[UniqueID]bool{0: true}},
					LockRange: LockRange{8192, LockEOF},
				},
			},
		},
		{
			name: "fill front gap with ReadLock",
			// + --------- + ---------------------------- +
			// | gap       | Readers 0                    |
			// + --------- + ---------------------------- +
			// 0        1024                     max uint64
			before: []entry{
				{
					Lock:      Lock{Readers: map[UniqueID]bool{0: true}},
					LockRange: LockRange{1024, LockEOF},
				},
			},
			start:    0,
			end:      8192,
			uid:      0,
			lockType: ReadLock,
			success:  true,
			// + ----------------------------------------- +
			// | Readers 0                                 |
			// + ----------------------------------------- +
			// 0                                  max uint64
			after: []entry{
				{
					Lock:      Lock{Readers: map[UniqueID]bool{0: true}},
					LockRange: LockRange{0, LockEOF},
				},
			},
		},
		{
			name: "fill end gap with ReadLock",
			// + ---------------------------- +
			// | Readers 0                    |
			// + ---------------------------- +
			// 0                           4096
			before: []entry{
				{
					Lock:      Lock{Readers: map[UniqueID]bool{0: true}},
					LockRange: LockRange{0, 4096},
				},
			},
			start:    1024,
			end:      LockEOF,
			uid:      0,
			lockType: ReadLock,
			success:  true,
			// Note that this is not merged after lock does a Split.  This is
			// fine because the two locks will still *behave* as one.  In other
			// words we can fragment any lock all we want and semantically it
			// makes no difference.
			//
			// + ----------- + --------------------------- +
			// | Readers 0   | Readers 0                   |
			// + ----------- + --------------------------- +
			// 0                                  max uint64
			after: []entry{
				{
					Lock:      Lock{Readers: map[UniqueID]bool{0: true}},
					LockRange: LockRange{0, 1024},
				},
				{
					Lock:      Lock{Readers: map[UniqueID]bool{0: true}},
					LockRange: LockRange{1024, LockEOF},
				},
			},
		},
		{
			name: "fill gap with ReadLock and split",
			// + --------- + ---------------------------- +
			// | gap       | Readers 0                    |
			// + --------- + ---------------------------- +
			// 0        1024                     max uint64
			before: []entry{
				{
					Lock:      Lock{Readers: map[UniqueID]bool{0: true}},
					LockRange: LockRange{1024, LockEOF},
				},
			},
			start:    0,
			end:      4096,
			uid:      1,
			lockType: ReadLock,
			success:  true,
			// + --------- + ------------- + ------------- +
			// | Reader 1  | Readers 0 & 1 | Reader 0      |
			// + ----------+ ------------- + ------------- +
			// 0        1024            4096      max uint64
			after: []entry{
				{
					Lock:      Lock{Readers: map[UniqueID]bool{1: true}},
					LockRange: LockRange{0, 1024},
				},
				{
					Lock:      Lock{Readers: map[UniqueID]bool{0: true, 1: true}},
					LockRange: LockRange{1024, 4096},
				},
				{
					Lock:      Lock{Readers: map[UniqueID]bool{0: true}},
					LockRange: LockRange{4096, LockEOF},
				},
			},
		},
		{
			name: "upgrade ReadLock to WriteLock for single uid fill gap",
			// + ------------- + --------- + --- + ------------- +
			// | Readers 0 & 1 | Readers 0 | gap | Readers 0 & 2 |
			// + ------------- + --------- + --- + ------------- +
			// 0            1024        2048  4096      max uint64
			before: []entry{
				{
					Lock:      Lock{Readers: map[UniqueID]bool{0: true, 1: true}},
					LockRange: LockRange{0, 1024},
				},
				{
					Lock:      Lock{Readers: map[UniqueID]bool{0: true}},
					LockRange: LockRange{1024, 2048},
				},
				{
					Lock:      Lock{Readers: map[UniqueID]bool{0: true, 2: true}},
					LockRange: LockRange{4096, LockEOF},
				},
			},
			start:    1024,
			end:      4096,
			uid:      0,
			lockType: WriteLock,
			success:  true,
			// + ------------- + -------- + ------------- +
			// | Readers 0 & 1 | Writer 0 | Readers 0 & 2 |
			// + ------------- + -------- + ------------- +
			// 0            1024       4096      max uint64
			after: []entry{
				{
					Lock:      Lock{Readers: map[UniqueID]bool{0: true, 1: true}},
					LockRange: LockRange{0, 1024},
				},
				{
					Lock:      Lock{Writer: 0},
					LockRange: LockRange{1024, 4096},
				},
				{
					Lock:      Lock{Readers: map[UniqueID]bool{0: true, 2: true}},
					LockRange: LockRange{4096, LockEOF},
				},
			},
		},
		{
			name: "upgrade ReadLock to WriteLock for single uid keep gap",
			// + ------------- + --------- + --- + ------------- +
			// | Readers 0 & 1 | Readers 0 | gap | Readers 0 & 2 |
			// + ------------- + --------- + --- + ------------- +
			// 0            1024        2048  4096      max uint64
			before: []entry{
				{
					Lock:      Lock{Readers: map[UniqueID]bool{0: true, 1: true}},
					LockRange: LockRange{0, 1024},
				},
				{
					Lock:      Lock{Readers: map[UniqueID]bool{0: true}},
					LockRange: LockRange{1024, 2048},
				},
				{
					Lock:      Lock{Readers: map[UniqueID]bool{0: true, 2: true}},
					LockRange: LockRange{4096, LockEOF},
				},
			},
			start:    1024,
			end:      3072,
			uid:      0,
			lockType: WriteLock,
			success:  true,
			// + ------------- + -------- + --- + ------------- +
			// | Readers 0 & 1 | Writer 0 | gap | Readers 0 & 2 |
			// + ------------- + -------- + --- + ------------- +
			// 0            1024       3072  4096      max uint64
			after: []entry{
				{
					Lock:      Lock{Readers: map[UniqueID]bool{0: true, 1: true}},
					LockRange: LockRange{0, 1024},
				},
				{
					Lock:      Lock{Writer: 0},
					LockRange: LockRange{1024, 3072},
				},
				{
					Lock:      Lock{Readers: map[UniqueID]bool{0: true, 2: true}},
					LockRange: LockRange{4096, LockEOF},
				},
			},
		},
		{
			name: "fail to upgrade ReadLock to WriteLock with conflicting Reader",
			// + ------------- + --------- +
			// | Readers 0 & 1 | Readers 0 |
			// + ------------- + --------- +
			// 0            1024        2048
			before: []entry{
				{
					Lock:      Lock{Readers: map[UniqueID]bool{0: true, 1: true}},
					LockRange: LockRange{0, 1024},
				},
				{
					Lock:      Lock{Readers: map[UniqueID]bool{0: true}},
					LockRange: LockRange{1024, 2048},
				},
			},
			start:    0,
			end:      2048,
			uid:      0,
			lockType: WriteLock,
			success:  false,
		},
		{
			name: "take WriteLock on whole file if all uids are the same",
			// + ------------- + --------- + --------- + ---------- +
			// | Writer 0      | Readers 0 | Readers 0 | Readers 0  |
			// + ------------- + --------- + --------- + ---------- +
			// 0            1024        2048        4096   max uint64
			before: []entry{
				{
					Lock:      Lock{Writer: 0},
					LockRange: LockRange{0, 1024},
				},
				{
					Lock:      Lock{Readers: map[UniqueID]bool{0: true}},
					LockRange: LockRange{1024, 2048},
				},
				{
					Lock:      Lock{Readers: map[UniqueID]bool{0: true}},
					LockRange: LockRange{2048, 4096},
				},
				{
					Lock:      Lock{Readers: map[UniqueID]bool{0: true}},
					LockRange: LockRange{4096, LockEOF},
				},
			},
			start:    0,
			end:      LockEOF,
			uid:      0,
			lockType: WriteLock,
			success:  true,
			// We do not manually merge locks.  Semantically a fragmented lock
			// held by the same uid will behave as one lock so it makes no difference.
			//
			// + ------------- + ---------------------------- +
			// | Writer 0      | Writer 0                     |
			// + ------------- + ---------------------------- +
			// 0            1024                     max uint64
			after: []entry{
				{
					Lock:      Lock{Writer: 0},
					LockRange: LockRange{0, 1024},
				},
				{
					Lock:      Lock{Writer: 0},
					LockRange: LockRange{1024, LockEOF},
				},
			},
		},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			l := fill(test.before)

			r := LockRange{Start: test.start, End: test.end}
			success := l.lock(test.uid, test.lockType, r)
			var got []entry
			for seg := l.FirstSegment(); seg.Ok(); seg = seg.NextSegment() {
				got = append(got, entry{
					Lock:      seg.Value(),
					LockRange: seg.Range(),
				})
			}

			if success != test.success {
				t.Errorf("setlock(%v, %+v, %d, %d) got success %v, want %v", test.before, r, test.uid, test.lockType, success, test.success)
				return
			}

			if success {
				if !equals(got, test.after) {
					t.Errorf("got set %+v, want %+v", got, test.after)
				}
			}
		})
	}
}

func TestUnlock(t *testing.T) {
	tests := []struct {
		// description of test.
		name string

		// LockSet entries to pre-fill.
		before []entry

		// Description of region to unlock:
		//
		// start is the file start of the lock.
		start uint64
		// end is the end file start of the lock.
		end uint64
		// uid of lock holder.
		uid UniqueID

		// Expected layout of the set after unlocking.
		after []entry
	}{
		{
			name:  "unlock zero length on empty set",
			start: 0,
			end:   0,
			uid:   0,
		},
		{
			name:  "unlock on empty set (no-op)",
			start: 0,
			end:   LockEOF,
			uid:   0,
		},
		{
			name: "unlock uid not locked (no-op)",
			// + --------------------------- +
			// | Readers 1 & 2               |
			// + --------------------------- +
			// 0                    max uint64
			before: []entry{
				{
					Lock:      Lock{Readers: map[UniqueID]bool{1: true, 2: true}},
					LockRange: LockRange{0, LockEOF},
				},
			},
			start: 1024,
			end:   4096,
			uid:   0,
			// + --------------------------- +
			// | Readers 1 & 2               |
			// + --------------------------- +
			// 0                    max uint64
			after: []entry{
				{
					Lock:      Lock{Readers: map[UniqueID]bool{1: true, 2: true}},
					LockRange: LockRange{0, LockEOF},
				},
			},
		},
		{
			name: "unlock ReadLock over entire file",
			// + ----------------------------------------- +
			// | Readers 0                                 |
			// + ----------------------------------------- +
			// 0                                  max uint64
			before: []entry{
				{
					Lock:      Lock{Readers: map[UniqueID]bool{0: true}},
					LockRange: LockRange{0, LockEOF},
				},
			},
			start: 0,
			end:   LockEOF,
			uid:   0,
		},
		{
			name: "unlock WriteLock over entire file",
			// + ----------------------------------------- +
			// | Writer 0                                  |
			// + ----------------------------------------- +
			// 0                                  max uint64
			before: []entry{
				{
					Lock:      Lock{Writer: 0},
					LockRange: LockRange{0, LockEOF},
				},
			},
			start: 0,
			end:   LockEOF,
			uid:   0,
		},
		{
			name: "unlock partial ReadLock (start)",
			// + ----------------------------------------- +
			// | Readers 0                                 |
			// + ----------------------------------------- +
			// 0                                  max uint64
			before: []entry{
				{
					Lock:      Lock{Readers: map[UniqueID]bool{0: true}},
					LockRange: LockRange{0, LockEOF},
				},
			},
			start: 0,
			end:   4096,
			uid:   0,
			// + ------ + --------------------------- +
			// | gap    | Readers 0                   |
			// +------- + --------------------------- +
			// 0     4096                    max uint64
			after: []entry{
				{
					Lock:      Lock{Readers: map[UniqueID]bool{0: true}},
					LockRange: LockRange{4096, LockEOF},
				},
			},
		},
		{
			name: "unlock partial WriteLock (start)",
			// + ----------------------------------------- +
			// | Writer  0                                 |
			// + ----------------------------------------- +
			// 0                                  max uint64
			before: []entry{
				{
					Lock:      Lock{Writer: 0},
					LockRange: LockRange{0, LockEOF},
				},
			},
			start: 0,
			end:   4096,
			uid:   0,
			// + ------ + --------------------------- +
			// | gap    | Writer  0                   |
			// +------- + --------------------------- +
			// 0     4096                    max uint64
			after: []entry{
				{
					Lock:      Lock{Writer: 0},
					LockRange: LockRange{4096, LockEOF},
				},
			},
		},
		{
			name: "unlock partial ReadLock (end)",
			// + ----------------------------------------- +
			// | Readers 0                                 |
			// + ----------------------------------------- +
			// 0                                  max uint64
			before: []entry{
				{
					Lock:      Lock{Readers: map[UniqueID]bool{0: true}},
					LockRange: LockRange{0, LockEOF},
				},
			},
			start: 4096,
			end:   LockEOF,
			uid:   0,
			// + --------------------------- +
			// | Readers 0                   |
			// +---------------------------- +
			// 0                          4096
			after: []entry{
				{
					Lock:      Lock{Readers: map[UniqueID]bool{0: true}},
					LockRange: LockRange{0, 4096},
				},
			},
		},
		{
			name: "unlock partial WriteLock (end)",
			// + ----------------------------------------- +
			// | Writer  0                                 |
			// + ----------------------------------------- +
			// 0                                  max uint64
			before: []entry{
				{
					Lock:      Lock{Writer: 0},
					LockRange: LockRange{0, LockEOF},
				},
			},
			start: 4096,
			end:   LockEOF,
			uid:   0,
			// + --------------------------- +
			// | Writer  0                   |
			// +---------------------------- +
			// 0                          4096
			after: []entry{
				{
					Lock:      Lock{Writer: 0},
					LockRange: LockRange{0, 4096},
				},
			},
		},
		{
			name: "unlock for single uid",
			// + ------------- + --------- + ------------------- +
			// | Readers 0 & 1 | Writer 0  | Readers 0 & 1 & 2   |
			// + ------------- + --------- + ------------------- +
			// 0            1024        4096            max uint64
			before: []entry{
				{
					Lock:      Lock{Readers: map[UniqueID]bool{0: true, 1: true}},
					LockRange: LockRange{0, 1024},
				},
				{
					Lock:      Lock{Writer: 0},
					LockRange: LockRange{1024, 4096},
				},
				{
					Lock:      Lock{Readers: map[UniqueID]bool{0: true, 1: true, 2: true}},
					LockRange: LockRange{4096, LockEOF},
				},
			},
			start: 0,
			end:   LockEOF,
			uid:   0,
			// + --------- + --- + --------------- +
			// | Readers 1 | gap | Readers 1 & 2   |
			// + --------- + --- + --------------- +
			// 0        1024  4096        max uint64
			after: []entry{
				{
					Lock:      Lock{Readers: map[UniqueID]bool{1: true}},
					LockRange: LockRange{0, 1024},
				},
				{
					Lock:      Lock{Readers: map[UniqueID]bool{1: true, 2: true}},
					LockRange: LockRange{4096, LockEOF},
				},
			},
		},
		{
			name: "unlock subsection locked",
			// + ------------------------------- +
			// | Readers 0 & 1 & 2               |
			// + ------------------------------- +
			// 0                        max uint64
			before: []entry{
				{
					Lock:      Lock{Readers: map[UniqueID]bool{0: true, 1: true, 2: true}},
					LockRange: LockRange{0, LockEOF},
				},
			},
			start: 1024,
			end:   4096,
			uid:   0,
			// + ----------------- + ------------- + ----------------- +
			// | Readers 0 & 1 & 2 | Readers 1 & 2 | Readers 0 & 1 & 2 |
			// + ----------------- + ------------- + ----------------- +
			// 0                1024            4096          max uint64
			after: []entry{
				{
					Lock:      Lock{Readers: map[UniqueID]bool{0: true, 1: true, 2: true}},
					LockRange: LockRange{0, 1024},
				},
				{
					Lock:      Lock{Readers: map[UniqueID]bool{1: true, 2: true}},
					LockRange: LockRange{1024, 4096},
				},
				{
					Lock:      Lock{Readers: map[UniqueID]bool{0: true, 1: true, 2: true}},
					LockRange: LockRange{4096, LockEOF},
				},
			},
		},
		{
			name: "unlock mid-gap to increase gap",
			// + --------- + ----- + ------------------- +
			// | Writer 0  |  gap  | Readers 0 & 1       |
			// + --------- + ----- + ------------------- +
			// 0        1024    4096            max uint64
			before: []entry{
				{
					Lock:      Lock{Writer: 0},
					LockRange: LockRange{0, 1024},
				},
				{
					Lock:      Lock{Readers: map[UniqueID]bool{0: true, 1: true}},
					LockRange: LockRange{4096, LockEOF},
				},
			},
			start: 8,
			end:   2048,
			uid:   0,
			// + --------- + ----- + ------------------- +
			// | Writer 0  |  gap  | Readers 0 & 1       |
			// + --------- + ----- + ------------------- +
			// 0           8    4096            max uint64
			after: []entry{
				{
					Lock:      Lock{Writer: 0},
					LockRange: LockRange{0, 8},
				},
				{
					Lock:      Lock{Readers: map[UniqueID]bool{0: true, 1: true}},
					LockRange: LockRange{4096, LockEOF},
				},
			},
		},
		{
			name: "unlock split region on uid mid-gap",
			// + --------- + ----- + ------------------- +
			// | Writer 0  |  gap  | Readers 0 & 1       |
			// + --------- + ----- + ------------------- +
			// 0        1024    4096            max uint64
			before: []entry{
				{
					Lock:      Lock{Writer: 0},
					LockRange: LockRange{0, 1024},
				},
				{
					Lock:      Lock{Readers: map[UniqueID]bool{0: true, 1: true}},
					LockRange: LockRange{4096, LockEOF},
				},
			},
			start: 2048,
			end:   8192,
			uid:   0,
			// + --------- + ----- + --------- + ------------- +
			// | Writer 0  |  gap  | Readers 1 | Readers 0 & 1 |
			// + --------- + ----- + --------- + ------------- +
			// 0       1024     4096        8192      max uint64
			after: []entry{
				{
					Lock:      Lock{Writer: 0},
					LockRange: LockRange{0, 1024},
				},
				{
					Lock:      Lock{Readers: map[UniqueID]bool{1: true}},
					LockRange: LockRange{4096, 8192},
				},
				{
					Lock:      Lock{Readers: map[UniqueID]bool{0: true, 1: true}},
					LockRange: LockRange{8192, LockEOF},
				},
			},
		},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			l := fill(test.before)

			r := LockRange{Start: test.start, End: test.end}
			l.unlock(test.uid, r)
			var got []entry
			for seg := l.FirstSegment(); seg.Ok(); seg = seg.NextSegment() {
				got = append(got, entry{
					Lock:      seg.Value(),
					LockRange: seg.Range(),
				})
			}
			if !equals(got, test.after) {
				t.Errorf("got set %+v, want %+v", got, test.after)
			}
		})
	}
}