blob: 44bfdfcf024fe4d8584f7fdfd2d005654414233f [file] [log] [blame]
package db
import (
"fmt"
"net/url"
"testing"
"time"
assert "github.com/stretchr/testify/require"
"go.skia.org/infra/go/testutils"
"go.skia.org/infra/go/util"
)
const DEFAULT_TEST_REPO = "go-on-now.git"
func makeTask(ts time.Time, commits []string) *Task {
return &Task{
Created: ts,
TaskKey: TaskKey{
RepoState: RepoState{
Repo: DEFAULT_TEST_REPO,
Revision: commits[0],
},
Name: "Test-Task",
},
Commits: commits,
SwarmingTaskId: "swarmid",
}
}
func makeJob(ts time.Time) *Job {
return &Job{
Created: ts,
Dependencies: map[string][]string{},
RepoState: RepoState{
Repo: DEFAULT_TEST_REPO,
},
Name: "Test-Job",
Tasks: map[string][]*TaskSummary{},
}
}
// TestTaskDB performs basic tests for an implementation of TaskDB.
func TestTaskDB(t *testing.T, db TaskDB) {
_, err := db.GetModifiedTasks("dummy-id")
assert.True(t, IsUnknownId(err))
id, err := db.StartTrackingModifiedTasks()
assert.NoError(t, err)
tasks, err := db.GetModifiedTasks(id)
assert.NoError(t, err)
assert.Equal(t, 0, len(tasks))
t1 := makeTask(time.Time{}, []string{"a", "b", "c", "d"})
// AssignId should fill in t1.Id.
assert.Equal(t, "", t1.Id)
assert.NoError(t, db.AssignId(t1))
assert.NotEqual(t, "", t1.Id)
// Ids must be URL-safe.
assert.Equal(t, url.QueryEscape(t1.Id), t1.Id)
// Task doesn't exist in DB yet.
noTask, err := db.GetTaskById(t1.Id)
assert.NoError(t, err)
assert.Nil(t, noTask)
// Set Creation time. Ensure Created is not the time of AssignId to test the
// sequence (1) AssignId, (2) initialize task, (3) PutTask.
now := time.Now().Add(time.Nanosecond)
t1.Created = now
// Insert the task.
assert.NoError(t, db.PutTask(t1))
// Check that DbModified was set.
assert.False(t, util.TimeIsZero(t1.DbModified))
t1LastModified := t1.DbModified
// Task can now be retrieved by Id.
t1Again, err := db.GetTaskById(t1.Id)
assert.NoError(t, err)
testutils.AssertDeepEqual(t, t1, t1Again)
// Ensure that the task shows up in the modified list.
tasks, err = db.GetModifiedTasks(id)
assert.NoError(t, err)
testutils.AssertDeepEqual(t, []*Task{t1}, tasks)
// Ensure that the task shows up in the correct date ranges.
timeStart := time.Time{}
t1Before := t1.Created
t1After := t1Before.Add(1 * time.Nanosecond)
timeEnd := now.Add(2 * time.Nanosecond)
tasks, err = db.GetTasksFromDateRange(timeStart, t1Before)
assert.NoError(t, err)
assert.Equal(t, 0, len(tasks))
tasks, err = db.GetTasksFromDateRange(t1Before, t1After)
assert.NoError(t, err)
testutils.AssertDeepEqual(t, []*Task{t1}, tasks)
tasks, err = db.GetTasksFromDateRange(t1After, timeEnd)
assert.NoError(t, err)
assert.Equal(t, 0, len(tasks))
// Insert two more tasks. Ensure at least 1 nanosecond between task Created
// times so that t1After != t2Before and t2After != t3Before.
t2 := makeTask(now.Add(time.Nanosecond), []string{"e", "f"})
t3 := makeTask(now.Add(2*time.Nanosecond), []string{"g", "h"})
assert.NoError(t, db.PutTasks([]*Task{t2, t3}))
// Check that PutTasks assigned Ids.
assert.NotEqual(t, "", t2.Id)
assert.NotEqual(t, "", t3.Id)
// Ids must be URL-safe.
assert.Equal(t, url.QueryEscape(t2.Id), t2.Id)
assert.Equal(t, url.QueryEscape(t3.Id), t3.Id)
// Ensure that both tasks show up in the modified list.
tasks, err = db.GetModifiedTasks(id)
assert.NoError(t, err)
testutils.AssertDeepEqual(t, []*Task{t2, t3}, tasks)
// Make an update to t1 and t2. Ensure modified times change.
t2LastModified := t2.DbModified
t1.Status = TASK_STATUS_RUNNING
t2.Status = TASK_STATUS_SUCCESS
assert.NoError(t, db.PutTasks([]*Task{t1, t2}))
assert.False(t, t1.DbModified.Equal(t1LastModified))
assert.False(t, t2.DbModified.Equal(t2LastModified))
// Ensure that both tasks show up in the modified list.
tasks, err = db.GetModifiedTasks(id)
assert.NoError(t, err)
testutils.AssertDeepEqual(t, []*Task{t1, t2}, tasks)
// Ensure that all tasks show up in the correct time ranges, in sorted order.
t2Before := t2.Created
t2After := t2Before.Add(1 * time.Nanosecond)
t3Before := t3.Created
t3After := t3Before.Add(1 * time.Nanosecond)
timeEnd = now.Add(3 * time.Nanosecond)
tasks, err = db.GetTasksFromDateRange(timeStart, t1Before)
assert.NoError(t, err)
assert.Equal(t, 0, len(tasks))
tasks, err = db.GetTasksFromDateRange(timeStart, t1After)
assert.NoError(t, err)
testutils.AssertDeepEqual(t, []*Task{t1}, tasks)
tasks, err = db.GetTasksFromDateRange(timeStart, t2Before)
assert.NoError(t, err)
testutils.AssertDeepEqual(t, []*Task{t1}, tasks)
tasks, err = db.GetTasksFromDateRange(timeStart, t2After)
assert.NoError(t, err)
testutils.AssertDeepEqual(t, []*Task{t1, t2}, tasks)
tasks, err = db.GetTasksFromDateRange(timeStart, t3Before)
assert.NoError(t, err)
testutils.AssertDeepEqual(t, []*Task{t1, t2}, tasks)
tasks, err = db.GetTasksFromDateRange(timeStart, t3After)
assert.NoError(t, err)
testutils.AssertDeepEqual(t, []*Task{t1, t2, t3}, tasks)
tasks, err = db.GetTasksFromDateRange(timeStart, timeEnd)
assert.NoError(t, err)
testutils.AssertDeepEqual(t, []*Task{t1, t2, t3}, tasks)
tasks, err = db.GetTasksFromDateRange(t1Before, timeEnd)
assert.NoError(t, err)
testutils.AssertDeepEqual(t, []*Task{t1, t2, t3}, tasks)
tasks, err = db.GetTasksFromDateRange(t1After, timeEnd)
assert.NoError(t, err)
testutils.AssertDeepEqual(t, []*Task{t2, t3}, tasks)
tasks, err = db.GetTasksFromDateRange(t2Before, timeEnd)
assert.NoError(t, err)
testutils.AssertDeepEqual(t, []*Task{t2, t3}, tasks)
tasks, err = db.GetTasksFromDateRange(t2After, timeEnd)
assert.NoError(t, err)
testutils.AssertDeepEqual(t, []*Task{t3}, tasks)
tasks, err = db.GetTasksFromDateRange(t3Before, timeEnd)
assert.NoError(t, err)
testutils.AssertDeepEqual(t, []*Task{t3}, tasks)
tasks, err = db.GetTasksFromDateRange(t3After, timeEnd)
assert.NoError(t, err)
testutils.AssertDeepEqual(t, []*Task{}, tasks)
}
// Test that a TaskDB properly tracks its maximum number of users.
func TestTaskDBTooManyUsers(t *testing.T, db TaskDB) {
// Max out the number of modified-tasks users; ensure that we error out.
for i := 0; i < MAX_MODIFIED_DATA_USERS; i++ {
_, err := db.StartTrackingModifiedTasks()
assert.NoError(t, err)
}
_, err := db.StartTrackingModifiedTasks()
assert.True(t, IsTooManyUsers(err))
}
// Test that PutTask and PutTasks return ErrConcurrentUpdate when a cached Task
// has been updated in the DB.
func TestTaskDBConcurrentUpdate(t *testing.T, db TaskDB) {
// Insert a task.
t1 := makeTask(time.Now(), []string{"a", "b", "c", "d"})
assert.NoError(t, db.PutTask(t1))
// Retrieve a copy of the task.
t1Cached, err := db.GetTaskById(t1.Id)
assert.NoError(t, err)
testutils.AssertDeepEqual(t, t1, t1Cached)
// Update the original task.
t1.Commits = []string{"a", "b"}
assert.NoError(t, db.PutTask(t1))
// Update the cached copy; should get concurrent update error.
t1Cached.Status = TASK_STATUS_RUNNING
err = db.PutTask(t1Cached)
assert.True(t, IsConcurrentUpdate(err))
{
// DB should still have the old value of t1.
t1Again, err := db.GetTaskById(t1.Id)
assert.NoError(t, err)
testutils.AssertDeepEqual(t, t1, t1Again)
}
// Insert a second task.
t2 := makeTask(time.Now(), []string{"e", "f"})
assert.NoError(t, db.PutTask(t2))
// Update t2 at the same time as t1Cached; should still get an error.
t2Before := t2.Copy()
t2.Status = TASK_STATUS_MISHAP
err = db.PutTasks([]*Task{t2, t1Cached})
assert.True(t, IsConcurrentUpdate(err))
{
// DB should still have the old value of t1 and t2.
t1Again, err := db.GetTaskById(t1.Id)
assert.NoError(t, err)
testutils.AssertDeepEqual(t, t1, t1Again)
t2Again, err := db.GetTaskById(t2.Id)
assert.NoError(t, err)
testutils.AssertDeepEqual(t, t2Before, t2Again)
}
}
// Test UpdateTasksWithRetries when no errors or retries.
func testUpdateTasksWithRetriesSimple(t *testing.T, db TaskDB) {
begin := time.Now()
// Test no-op.
tasks, err := UpdateTasksWithRetries(db, func() ([]*Task, error) {
return nil, nil
})
assert.NoError(t, err)
assert.Equal(t, 0, len(tasks))
// Create new task t1. (UpdateTasksWithRetries isn't actually useful in this case.)
tasks, err = UpdateTasksWithRetries(db, func() ([]*Task, error) {
t1 := makeTask(time.Time{}, []string{"a", "b", "c", "d"})
assert.NoError(t, db.AssignId(t1))
t1.Created = time.Now().Add(time.Nanosecond)
return []*Task{t1}, nil
})
assert.NoError(t, err)
assert.Equal(t, 1, len(tasks))
t1 := tasks[0]
// Update t1 and create t2.
tasks, err = UpdateTasksWithRetries(db, func() ([]*Task, error) {
t1, err := db.GetTaskById(t1.Id)
assert.NoError(t, err)
t1.Status = TASK_STATUS_RUNNING
t2 := makeTask(t1.Created.Add(time.Nanosecond), []string{"e", "f"})
return []*Task{t1, t2}, nil
})
assert.NoError(t, err)
assert.Equal(t, 2, len(tasks))
assert.Equal(t, t1.Id, tasks[0].Id)
assert.Equal(t, TASK_STATUS_RUNNING, tasks[0].Status)
assert.Equal(t, []string{"e", "f"}, tasks[1].Commits)
// Check that return value matches what's in the DB.
t1, err = db.GetTaskById(t1.Id)
assert.NoError(t, err)
t2, err := db.GetTaskById(tasks[1].Id)
assert.NoError(t, err)
testutils.AssertDeepEqual(t, tasks[0], t1)
testutils.AssertDeepEqual(t, tasks[1], t2)
// Check no extra tasks in the DB.
tasks, err = db.GetTasksFromDateRange(begin, time.Now().Add(3*time.Nanosecond))
assert.NoError(t, err)
assert.Equal(t, 2, len(tasks))
assert.Equal(t, t1.Id, tasks[0].Id)
assert.Equal(t, t2.Id, tasks[1].Id)
}
// Test UpdateTasksWithRetries when there are some retries, but eventual success.
func testUpdateTasksWithRetriesSuccess(t *testing.T, db TaskDB) {
begin := time.Now()
// Create and cache.
t1 := makeTask(begin.Add(time.Nanosecond), []string{"a", "b", "c", "d"})
assert.NoError(t, db.PutTask(t1))
t1Cached := t1.Copy()
// Update original.
t1.Status = TASK_STATUS_RUNNING
assert.NoError(t, db.PutTask(t1))
// Attempt update.
callCount := 0
tasks, err := UpdateTasksWithRetries(db, func() ([]*Task, error) {
callCount++
if callCount >= 3 {
if task, err := db.GetTaskById(t1.Id); err != nil {
return nil, err
} else {
t1Cached = task
}
}
t1Cached.Status = TASK_STATUS_SUCCESS
t2 := makeTask(begin.Add(2*time.Nanosecond), []string{"e", "f"})
return []*Task{t1Cached, t2}, nil
})
assert.NoError(t, err)
assert.Equal(t, 3, callCount)
assert.Equal(t, 2, len(tasks))
assert.Equal(t, t1.Id, tasks[0].Id)
assert.Equal(t, TASK_STATUS_SUCCESS, tasks[0].Status)
assert.Equal(t, []string{"e", "f"}, tasks[1].Commits)
// Check that return value matches what's in the DB.
t1, err = db.GetTaskById(t1.Id)
assert.NoError(t, err)
t2, err := db.GetTaskById(tasks[1].Id)
assert.NoError(t, err)
testutils.AssertDeepEqual(t, tasks[0], t1)
testutils.AssertDeepEqual(t, tasks[1], t2)
// Check no extra tasks in the DB.
tasks, err = db.GetTasksFromDateRange(begin, time.Now().Add(3*time.Nanosecond))
assert.NoError(t, err)
assert.Equal(t, 2, len(tasks))
assert.Equal(t, t1.Id, tasks[0].Id)
assert.Equal(t, t2.Id, tasks[1].Id)
}
// Test UpdateTasksWithRetries when f returns an error.
func testUpdateTasksWithRetriesErrorInFunc(t *testing.T, db TaskDB) {
begin := time.Now()
myErr := fmt.Errorf("NO! Bad dog!")
callCount := 0
tasks, err := UpdateTasksWithRetries(db, func() ([]*Task, error) {
callCount++
// Return a task just for fun.
return []*Task{
makeTask(begin.Add(time.Nanosecond), []string{"a", "b", "c", "d"}),
}, myErr
})
assert.Error(t, err)
assert.Equal(t, myErr, err)
assert.Equal(t, 0, len(tasks))
assert.Equal(t, 1, callCount)
// Check no tasks in the DB.
tasks, err = db.GetTasksFromDateRange(begin, time.Now().Add(2*time.Nanosecond))
assert.NoError(t, err)
assert.Equal(t, 0, len(tasks))
}
// Test UpdateTasksWithRetries when PutTasks returns an error.
func testUpdateTasksWithRetriesErrorInPutTasks(t *testing.T, db TaskDB) {
begin := time.Now()
callCount := 0
tasks, err := UpdateTasksWithRetries(db, func() ([]*Task, error) {
callCount++
// Task has zero Created time.
return []*Task{
makeTask(time.Time{}, []string{"a", "b", "c", "d"}),
}, nil
})
assert.Error(t, err)
assert.Contains(t, err.Error(), "Created not set.")
assert.Equal(t, 0, len(tasks))
assert.Equal(t, 1, callCount)
// Check no tasks in the DB.
tasks, err = db.GetTasksFromDateRange(begin, time.Now().Add(time.Nanosecond))
assert.NoError(t, err)
assert.Equal(t, 0, len(tasks))
}
// Test UpdateTasksWithRetries when retries are exhausted.
func testUpdateTasksWithRetriesExhausted(t *testing.T, db TaskDB) {
begin := time.Now()
// Create and cache.
t1 := makeTask(begin.Add(time.Nanosecond), []string{"a", "b", "c", "d"})
assert.NoError(t, db.PutTask(t1))
t1Cached := t1.Copy()
// Update original.
t1.Status = TASK_STATUS_RUNNING
assert.NoError(t, db.PutTask(t1))
// Attempt update.
callCount := 0
tasks, err := UpdateTasksWithRetries(db, func() ([]*Task, error) {
callCount++
t1Cached.Status = TASK_STATUS_SUCCESS
t2 := makeTask(begin.Add(2*time.Nanosecond), []string{"e", "f"})
return []*Task{t1Cached, t2}, nil
})
assert.True(t, IsConcurrentUpdate(err))
assert.Equal(t, NUM_RETRIES, callCount)
assert.Equal(t, 0, len(tasks))
// Check no extra tasks in the DB.
tasks, err = db.GetTasksFromDateRange(begin, time.Now().Add(3*time.Nanosecond))
assert.NoError(t, err)
assert.Equal(t, 1, len(tasks))
assert.Equal(t, t1.Id, tasks[0].Id)
assert.Equal(t, TASK_STATUS_RUNNING, tasks[0].Status)
}
// Test UpdateTaskWithRetries when no errors or retries.
func testUpdateTaskWithRetriesSimple(t *testing.T, db TaskDB) {
begin := time.Now()
// Create new task t1.
t1 := makeTask(time.Time{}, []string{"a", "b", "c", "d"})
assert.NoError(t, db.AssignId(t1))
t1.Created = time.Now().Add(time.Nanosecond)
assert.NoError(t, db.PutTask(t1))
// Update t1.
t1Updated, err := UpdateTaskWithRetries(db, t1.Id, func(task *Task) error {
task.Status = TASK_STATUS_RUNNING
return nil
})
assert.NoError(t, err)
assert.Equal(t, t1.Id, t1Updated.Id)
assert.Equal(t, TASK_STATUS_RUNNING, t1Updated.Status)
assert.NotEqual(t, t1.DbModified, t1Updated.DbModified)
// Check that return value matches what's in the DB.
t1Again, err := db.GetTaskById(t1.Id)
assert.NoError(t, err)
testutils.AssertDeepEqual(t, t1Again, t1Updated)
// Check no extra tasks in the TaskDB.
tasks, err := db.GetTasksFromDateRange(begin, time.Now().Add(2*time.Nanosecond))
assert.NoError(t, err)
assert.Equal(t, 1, len(tasks))
assert.Equal(t, t1.Id, tasks[0].Id)
}
// Test UpdateTaskWithRetries when there are some retries, but eventual success.
func testUpdateTaskWithRetriesSuccess(t *testing.T, db TaskDB) {
begin := time.Now()
// Create new task t1.
t1 := makeTask(begin.Add(time.Nanosecond), []string{"a", "b", "c", "d"})
assert.NoError(t, db.PutTask(t1))
// Attempt update.
callCount := 0
t1Updated, err := UpdateTaskWithRetries(db, t1.Id, func(task *Task) error {
callCount++
if callCount < 3 {
// Sneakily make an update in the background.
t1.Commits = append(t1.Commits, fmt.Sprintf("z%d", callCount))
assert.NoError(t, db.PutTask(t1))
}
task.Status = TASK_STATUS_SUCCESS
return nil
})
assert.NoError(t, err)
assert.Equal(t, 3, callCount)
assert.Equal(t, t1.Id, t1Updated.Id)
assert.Equal(t, TASK_STATUS_SUCCESS, t1Updated.Status)
// Check that return value matches what's in the DB.
t1Again, err := db.GetTaskById(t1.Id)
assert.NoError(t, err)
testutils.AssertDeepEqual(t, t1Again, t1Updated)
// Check no extra tasks in the DB.
tasks, err := db.GetTasksFromDateRange(begin, time.Now().Add(2*time.Nanosecond))
assert.NoError(t, err)
assert.Equal(t, 1, len(tasks))
assert.Equal(t, t1.Id, tasks[0].Id)
}
// Test UpdateTaskWithRetries when f returns an error.
func testUpdateTaskWithRetriesErrorInFunc(t *testing.T, db TaskDB) {
begin := time.Now()
// Create new task t1.
t1 := makeTask(begin.Add(time.Nanosecond), []string{"a", "b", "c", "d"})
assert.NoError(t, db.PutTask(t1))
// Update and return an error.
myErr := fmt.Errorf("Um, actually, I didn't want to update that task.")
callCount := 0
noTask, err := UpdateTaskWithRetries(db, t1.Id, func(task *Task) error {
callCount++
// Update task to test nothing changes in DB.
task.Status = TASK_STATUS_RUNNING
return myErr
})
assert.Error(t, err)
assert.Equal(t, myErr, err)
assert.Nil(t, noTask)
assert.Equal(t, 1, callCount)
// Check task did not change in the DB.
t1Again, err := db.GetTaskById(t1.Id)
assert.NoError(t, err)
testutils.AssertDeepEqual(t, t1, t1Again)
// Check no extra tasks in the DB.
tasks, err := db.GetTasksFromDateRange(begin, time.Now().Add(2*time.Nanosecond))
assert.NoError(t, err)
assert.Equal(t, 1, len(tasks))
assert.Equal(t, t1.Id, tasks[0].Id)
}
// Test UpdateTaskWithRetries when retries are exhausted.
func testUpdateTaskWithRetriesExhausted(t *testing.T, db TaskDB) {
begin := time.Now()
// Create new task t1.
t1 := makeTask(begin.Add(time.Nanosecond), []string{"a", "b", "c", "d"})
assert.NoError(t, db.PutTask(t1))
// Update original.
t1.Status = TASK_STATUS_RUNNING
assert.NoError(t, db.PutTask(t1))
// Attempt update.
callCount := 0
noTask, err := UpdateTaskWithRetries(db, t1.Id, func(task *Task) error {
callCount++
// Sneakily make an update in the background.
t1.Commits = append(t1.Commits, fmt.Sprintf("z%d", callCount))
assert.NoError(t, db.PutTask(t1))
task.Status = TASK_STATUS_SUCCESS
return nil
})
assert.True(t, IsConcurrentUpdate(err))
assert.Equal(t, NUM_RETRIES, callCount)
assert.Nil(t, noTask)
// Check task did not change in the DB.
t1Again, err := db.GetTaskById(t1.Id)
assert.NoError(t, err)
testutils.AssertDeepEqual(t, t1, t1Again)
// Check no extra tasks in the DB.
tasks, err := db.GetTasksFromDateRange(begin, time.Now().Add(2*time.Nanosecond))
assert.NoError(t, err)
assert.Equal(t, 1, len(tasks))
assert.Equal(t, t1.Id, tasks[0].Id)
}
// Test UpdateTaskWithRetries when the given ID is not found in the DB.
func testUpdateTaskWithRetriesTaskNotFound(t *testing.T, db TaskDB) {
begin := time.Now()
// Assign ID for a task, but don't put it in the DB.
t1 := makeTask(begin.Add(time.Nanosecond), []string{"a", "b", "c", "d"})
assert.NoError(t, db.AssignId(t1))
// Attempt to update non-existent task. Function shouldn't be called.
callCount := 0
noTask, err := UpdateTaskWithRetries(db, t1.Id, func(task *Task) error {
callCount++
task.Status = TASK_STATUS_RUNNING
return nil
})
assert.True(t, IsNotFound(err))
assert.Nil(t, noTask)
assert.Equal(t, 0, callCount)
// Check no tasks in the DB.
tasks, err := db.GetTasksFromDateRange(begin, time.Now().Add(2*time.Nanosecond))
assert.NoError(t, err)
assert.Equal(t, 0, len(tasks))
}
// Test UpdateTasksWithRetries and UpdateTaskWithRetries.
func TestUpdateTasksWithRetries(t *testing.T, db TaskDB) {
testUpdateTasksWithRetriesSimple(t, db)
testUpdateTasksWithRetriesSuccess(t, db)
testUpdateTasksWithRetriesErrorInFunc(t, db)
testUpdateTasksWithRetriesErrorInPutTasks(t, db)
testUpdateTasksWithRetriesExhausted(t, db)
testUpdateTaskWithRetriesSimple(t, db)
testUpdateTaskWithRetriesSuccess(t, db)
testUpdateTaskWithRetriesErrorInFunc(t, db)
testUpdateTaskWithRetriesExhausted(t, db)
testUpdateTaskWithRetriesTaskNotFound(t, db)
}
// TestJobDB performs basic tests on an implementation of JobDB.
func TestJobDB(t *testing.T, db JobDB) {
_, err := db.GetModifiedJobs("dummy-id")
assert.True(t, IsUnknownId(err))
id, err := db.StartTrackingModifiedJobs()
assert.NoError(t, err)
jobs, err := db.GetModifiedJobs(id)
assert.NoError(t, err)
assert.Equal(t, 0, len(jobs))
now := time.Now().Add(time.Nanosecond)
j1 := makeJob(now)
// Insert the job.
assert.NoError(t, db.PutJob(j1))
// Ids must be URL-safe.
assert.NotEqual(t, "", j1.Id)
assert.Equal(t, url.QueryEscape(j1.Id), j1.Id)
// Check that DbModified was set.
assert.False(t, util.TimeIsZero(j1.DbModified))
j1LastModified := j1.DbModified
// Job can now be retrieved by Id.
j1Again, err := db.GetJobById(j1.Id)
assert.NoError(t, err)
testutils.AssertDeepEqual(t, j1, j1Again)
// Ensure that the job shows up in the modified list.
jobs, err = db.GetModifiedJobs(id)
assert.NoError(t, err)
testutils.AssertDeepEqual(t, []*Job{j1}, jobs)
// Ensure that the job shows up in the correct date ranges.
timeStart := time.Time{}
j1Before := j1.Created
j1After := j1Before.Add(1 * time.Nanosecond)
timeEnd := now.Add(2 * time.Nanosecond)
jobs, err = db.GetJobsFromDateRange(timeStart, j1Before)
assert.NoError(t, err)
assert.Equal(t, 0, len(jobs))
jobs, err = db.GetJobsFromDateRange(j1Before, j1After)
assert.NoError(t, err)
testutils.AssertDeepEqual(t, []*Job{j1}, jobs)
jobs, err = db.GetJobsFromDateRange(j1After, timeEnd)
assert.NoError(t, err)
assert.Equal(t, 0, len(jobs))
// Insert two more jobs. Ensure at least 1 nanosecond between job Created
// times so that j1After != j2Before and j2After != j3Before.
j2 := makeJob(now.Add(time.Nanosecond))
j3 := makeJob(now.Add(2 * time.Nanosecond))
assert.NoError(t, db.PutJobs([]*Job{j2, j3}))
// Check that PutJobs assigned Ids.
assert.NotEqual(t, "", j2.Id)
assert.NotEqual(t, "", j3.Id)
// Ids must be URL-safe.
assert.Equal(t, url.QueryEscape(j2.Id), j2.Id)
assert.Equal(t, url.QueryEscape(j3.Id), j3.Id)
// Ensure that both jobs show up in the modified list.
jobs, err = db.GetModifiedJobs(id)
assert.NoError(t, err)
testutils.AssertDeepEqual(t, []*Job{j2, j3}, jobs)
// Make an update to j1 and j2. Ensure modified times change.
j2LastModified := j2.DbModified
j1.Status = JOB_STATUS_IN_PROGRESS
j2.Status = JOB_STATUS_SUCCESS
assert.NoError(t, db.PutJobs([]*Job{j1, j2}))
assert.False(t, j1.DbModified.Equal(j1LastModified))
assert.False(t, j2.DbModified.Equal(j2LastModified))
// Ensure that both jobs show up in the modified list.
jobs, err = db.GetModifiedJobs(id)
assert.NoError(t, err)
testutils.AssertDeepEqual(t, []*Job{j1, j2}, jobs)
// Ensure that all jobs show up in the correct time ranges, in sorted order.
j2Before := j2.Created
j2After := j2Before.Add(1 * time.Nanosecond)
j3Before := j3.Created
j3After := j3Before.Add(1 * time.Nanosecond)
timeEnd = now.Add(3 * time.Nanosecond)
jobs, err = db.GetJobsFromDateRange(timeStart, j1Before)
assert.NoError(t, err)
assert.Equal(t, 0, len(jobs))
jobs, err = db.GetJobsFromDateRange(timeStart, j1After)
assert.NoError(t, err)
testutils.AssertDeepEqual(t, []*Job{j1}, jobs)
jobs, err = db.GetJobsFromDateRange(timeStart, j2Before)
assert.NoError(t, err)
testutils.AssertDeepEqual(t, []*Job{j1}, jobs)
jobs, err = db.GetJobsFromDateRange(timeStart, j2After)
assert.NoError(t, err)
testutils.AssertDeepEqual(t, []*Job{j1, j2}, jobs)
jobs, err = db.GetJobsFromDateRange(timeStart, j3Before)
assert.NoError(t, err)
testutils.AssertDeepEqual(t, []*Job{j1, j2}, jobs)
jobs, err = db.GetJobsFromDateRange(timeStart, j3After)
assert.NoError(t, err)
testutils.AssertDeepEqual(t, []*Job{j1, j2, j3}, jobs)
jobs, err = db.GetJobsFromDateRange(timeStart, timeEnd)
assert.NoError(t, err)
testutils.AssertDeepEqual(t, []*Job{j1, j2, j3}, jobs)
jobs, err = db.GetJobsFromDateRange(j1Before, timeEnd)
assert.NoError(t, err)
testutils.AssertDeepEqual(t, []*Job{j1, j2, j3}, jobs)
jobs, err = db.GetJobsFromDateRange(j1After, timeEnd)
assert.NoError(t, err)
testutils.AssertDeepEqual(t, []*Job{j2, j3}, jobs)
jobs, err = db.GetJobsFromDateRange(j2Before, timeEnd)
assert.NoError(t, err)
testutils.AssertDeepEqual(t, []*Job{j2, j3}, jobs)
jobs, err = db.GetJobsFromDateRange(j2After, timeEnd)
assert.NoError(t, err)
testutils.AssertDeepEqual(t, []*Job{j3}, jobs)
jobs, err = db.GetJobsFromDateRange(j3Before, timeEnd)
assert.NoError(t, err)
testutils.AssertDeepEqual(t, []*Job{j3}, jobs)
jobs, err = db.GetJobsFromDateRange(j3After, timeEnd)
assert.NoError(t, err)
testutils.AssertDeepEqual(t, []*Job{}, jobs)
}
// Test that a JobDB properly tracks its maximum number of users.
func TestJobDBTooManyUsers(t *testing.T, db JobDB) {
// Max out the number of modified-jobs users; ensure that we error out.
for i := 0; i < MAX_MODIFIED_DATA_USERS; i++ {
_, err := db.StartTrackingModifiedJobs()
assert.NoError(t, err)
}
_, err := db.StartTrackingModifiedJobs()
assert.True(t, IsTooManyUsers(err))
}
// Test that PutJob and PutJobs return ErrConcurrentUpdate when a cached Job
// has been updated in the DB.
func TestJobDBConcurrentUpdate(t *testing.T, db JobDB) {
// Insert a job.
j1 := makeJob(time.Now())
assert.NoError(t, db.PutJob(j1))
// Retrieve a copy of the job.
j1Cached, err := db.GetJobById(j1.Id)
assert.NoError(t, err)
testutils.AssertDeepEqual(t, j1, j1Cached)
// Update the original job.
j1.Repo = "another-repo"
assert.NoError(t, db.PutJob(j1))
// Update the cached copy; should get concurrent update error.
j1Cached.Status = JOB_STATUS_IN_PROGRESS
err = db.PutJob(j1Cached)
assert.True(t, IsConcurrentUpdate(err))
{
// DB should still have the old value of j1.
j1Again, err := db.GetJobById(j1.Id)
assert.NoError(t, err)
testutils.AssertDeepEqual(t, j1, j1Again)
}
// Insert a second job.
j2 := makeJob(time.Now())
assert.NoError(t, db.PutJob(j2))
// Update j2 at the same time as j1Cached; should still get an error.
j2Before := j2.Copy()
j2.Status = JOB_STATUS_MISHAP
err = db.PutJobs([]*Job{j2, j1Cached})
assert.True(t, IsConcurrentUpdate(err))
{
// DB should still have the old value of j1 and j2.
j1Again, err := db.GetJobById(j1.Id)
assert.NoError(t, err)
testutils.AssertDeepEqual(t, j1, j1Again)
j2Again, err := db.GetJobById(j2.Id)
assert.NoError(t, err)
testutils.AssertDeepEqual(t, j2Before, j2Again)
}
}
// Test UpdateJobsWithRetries when no errors or retries.
func testUpdateJobsWithRetriesSimple(t *testing.T, db JobDB) {
begin := time.Now()
// Test no-op.
jobs, err := UpdateJobsWithRetries(db, func() ([]*Job, error) {
return nil, nil
})
assert.NoError(t, err)
assert.Equal(t, 0, len(jobs))
// Create new job j1. (UpdateJobsWithRetries isn't actually useful in this case.)
jobs, err = UpdateJobsWithRetries(db, func() ([]*Job, error) {
j1 := makeJob(time.Time{})
j1.Created = time.Now().Add(time.Nanosecond)
return []*Job{j1}, nil
})
assert.NoError(t, err)
assert.Equal(t, 1, len(jobs))
j1 := jobs[0]
// Update j1 and create j2.
jobs, err = UpdateJobsWithRetries(db, func() ([]*Job, error) {
j1, err := db.GetJobById(j1.Id)
assert.NoError(t, err)
j1.Status = JOB_STATUS_IN_PROGRESS
j2 := makeJob(j1.Created.Add(time.Nanosecond))
j2.Repo = "j2-repo"
return []*Job{j1, j2}, nil
})
assert.NoError(t, err)
assert.Equal(t, 2, len(jobs))
assert.Equal(t, j1.Id, jobs[0].Id)
assert.Equal(t, JOB_STATUS_IN_PROGRESS, jobs[0].Status)
assert.Equal(t, "j2-repo", jobs[1].Repo)
// Check that return value matches what's in the DB.
j1, err = db.GetJobById(j1.Id)
assert.NoError(t, err)
j2, err := db.GetJobById(jobs[1].Id)
assert.NoError(t, err)
testutils.AssertDeepEqual(t, jobs[0], j1)
testutils.AssertDeepEqual(t, jobs[1], j2)
// Check no extra jobs in the DB.
jobs, err = db.GetJobsFromDateRange(begin, time.Now().Add(3*time.Nanosecond))
assert.NoError(t, err)
assert.Equal(t, 2, len(jobs))
assert.Equal(t, j1.Id, jobs[0].Id)
assert.Equal(t, j2.Id, jobs[1].Id)
}
// Test UpdateJobsWithRetries when there are some retries, but eventual success.
func testUpdateJobsWithRetriesSuccess(t *testing.T, db JobDB) {
begin := time.Now()
// Create and cache.
j1 := makeJob(begin.Add(time.Nanosecond))
assert.NoError(t, db.PutJob(j1))
j1Cached := j1.Copy()
// Update original.
j1.Status = JOB_STATUS_IN_PROGRESS
assert.NoError(t, db.PutJob(j1))
// Attempt update.
callCount := 0
jobs, err := UpdateJobsWithRetries(db, func() ([]*Job, error) {
callCount++
if callCount >= 3 {
if job, err := db.GetJobById(j1.Id); err != nil {
return nil, err
} else {
j1Cached = job
}
}
j1Cached.Status = JOB_STATUS_SUCCESS
j2 := makeJob(begin.Add(2 * time.Nanosecond))
j2.Repo = "j2-repo"
return []*Job{j1Cached, j2}, nil
})
assert.NoError(t, err)
assert.Equal(t, 3, callCount)
assert.Equal(t, 2, len(jobs))
assert.Equal(t, j1.Id, jobs[0].Id)
assert.Equal(t, JOB_STATUS_SUCCESS, jobs[0].Status)
assert.Equal(t, "j2-repo", jobs[1].Repo)
// Check that return value matches what's in the DB.
j1, err = db.GetJobById(j1.Id)
assert.NoError(t, err)
j2, err := db.GetJobById(jobs[1].Id)
assert.NoError(t, err)
testutils.AssertDeepEqual(t, jobs[0], j1)
testutils.AssertDeepEqual(t, jobs[1], j2)
// Check no extra jobs in the DB.
jobs, err = db.GetJobsFromDateRange(begin, time.Now().Add(3*time.Nanosecond))
assert.NoError(t, err)
assert.Equal(t, 2, len(jobs))
assert.Equal(t, j1.Id, jobs[0].Id)
assert.Equal(t, j2.Id, jobs[1].Id)
}
// Test UpdateJobsWithRetries when f returns an error.
func testUpdateJobsWithRetriesErrorInFunc(t *testing.T, db JobDB) {
begin := time.Now()
myErr := fmt.Errorf("NO! Bad dog!")
callCount := 0
jobs, err := UpdateJobsWithRetries(db, func() ([]*Job, error) {
callCount++
// Return a job just for fun.
return []*Job{
makeJob(begin.Add(time.Nanosecond)),
}, myErr
})
assert.Error(t, err)
assert.Equal(t, myErr, err)
assert.Equal(t, 0, len(jobs))
assert.Equal(t, 1, callCount)
// Check no jobs in the DB.
jobs, err = db.GetJobsFromDateRange(begin, time.Now().Add(2*time.Nanosecond))
assert.NoError(t, err)
assert.Equal(t, 0, len(jobs))
}
// Test UpdateJobsWithRetries when PutJobs returns an error.
func testUpdateJobsWithRetriesErrorInPutJobs(t *testing.T, db JobDB) {
begin := time.Now()
callCount := 0
jobs, err := UpdateJobsWithRetries(db, func() ([]*Job, error) {
callCount++
// Job has zero Created time.
return []*Job{
makeJob(time.Time{}),
}, nil
})
assert.Error(t, err)
assert.Contains(t, err.Error(), "Created not set.")
assert.Equal(t, 0, len(jobs))
assert.Equal(t, 1, callCount)
// Check no jobs in the DB.
jobs, err = db.GetJobsFromDateRange(begin, time.Now().Add(time.Nanosecond))
assert.NoError(t, err)
assert.Equal(t, 0, len(jobs))
}
// Test UpdateJobsWithRetries when retries are exhausted.
func testUpdateJobsWithRetriesExhausted(t *testing.T, db JobDB) {
begin := time.Now()
// Create and cache.
j1 := makeJob(begin.Add(time.Nanosecond))
assert.NoError(t, db.PutJob(j1))
j1Cached := j1.Copy()
// Update original.
j1.Status = JOB_STATUS_IN_PROGRESS
assert.NoError(t, db.PutJob(j1))
// Attempt update.
callCount := 0
jobs, err := UpdateJobsWithRetries(db, func() ([]*Job, error) {
callCount++
j1Cached.Status = JOB_STATUS_SUCCESS
j2 := makeJob(begin.Add(2 * time.Nanosecond))
return []*Job{j1Cached, j2}, nil
})
assert.True(t, IsConcurrentUpdate(err))
assert.Equal(t, NUM_RETRIES, callCount)
assert.Equal(t, 0, len(jobs))
// Check no extra jobs in the DB.
jobs, err = db.GetJobsFromDateRange(begin, time.Now().Add(3*time.Nanosecond))
assert.NoError(t, err)
assert.Equal(t, 1, len(jobs))
assert.Equal(t, j1.Id, jobs[0].Id)
assert.Equal(t, JOB_STATUS_IN_PROGRESS, jobs[0].Status)
}
// Test UpdateJobWithRetries when no errors or retries.
func testUpdateJobWithRetriesSimple(t *testing.T, db JobDB) {
begin := time.Now()
// Create new job j1.
j1 := makeJob(time.Now())
assert.NoError(t, db.PutJob(j1))
// Update j1.
j1Updated, err := UpdateJobWithRetries(db, j1.Id, func(job *Job) error {
job.Status = JOB_STATUS_IN_PROGRESS
return nil
})
assert.NoError(t, err)
assert.Equal(t, j1.Id, j1Updated.Id)
assert.Equal(t, JOB_STATUS_IN_PROGRESS, j1Updated.Status)
assert.NotEqual(t, j1.DbModified, j1Updated.DbModified)
// Check that return value matches what's in the DB.
j1Again, err := db.GetJobById(j1.Id)
assert.NoError(t, err)
testutils.AssertDeepEqual(t, j1Again, j1Updated)
// Check no extra jobs in the JobDB.
jobs, err := db.GetJobsFromDateRange(begin, time.Now().Add(2*time.Nanosecond))
assert.NoError(t, err)
assert.Equal(t, 1, len(jobs))
assert.Equal(t, j1.Id, jobs[0].Id)
}
// Test UpdateJobWithRetries when there are some retries, but eventual success.
func testUpdateJobWithRetriesSuccess(t *testing.T, db JobDB) {
begin := time.Now()
// Create new job j1.
j1 := makeJob(begin.Add(time.Nanosecond))
assert.NoError(t, db.PutJob(j1))
// Attempt update.
callCount := 0
j1Updated, err := UpdateJobWithRetries(db, j1.Id, func(job *Job) error {
callCount++
if callCount < 3 {
// Sneakily make an update in the background.
j1.Repo = "some-other-repo.git"
assert.NoError(t, db.PutJob(j1))
}
job.Status = JOB_STATUS_SUCCESS
return nil
})
assert.NoError(t, err)
assert.Equal(t, 3, callCount)
assert.Equal(t, j1.Id, j1Updated.Id)
assert.Equal(t, JOB_STATUS_SUCCESS, j1Updated.Status)
// Check that return value matches what's in the DB.
j1Again, err := db.GetJobById(j1.Id)
assert.NoError(t, err)
testutils.AssertDeepEqual(t, j1Again, j1Updated)
// Check no extra jobs in the DB.
jobs, err := db.GetJobsFromDateRange(begin, time.Now().Add(2*time.Nanosecond))
assert.NoError(t, err)
assert.Equal(t, 1, len(jobs))
assert.Equal(t, j1.Id, jobs[0].Id)
}
// Test UpdateJobWithRetries when f returns an error.
func testUpdateJobWithRetriesErrorInFunc(t *testing.T, db JobDB) {
begin := time.Now()
// Create new job j1.
j1 := makeJob(begin.Add(time.Nanosecond))
assert.NoError(t, db.PutJob(j1))
// Update and return an error.
myErr := fmt.Errorf("Um, actually, I didn't want to update that job.")
callCount := 0
noJob, err := UpdateJobWithRetries(db, j1.Id, func(job *Job) error {
callCount++
// Update job to test nothing changes in DB.
job.Status = JOB_STATUS_IN_PROGRESS
return myErr
})
assert.Error(t, err)
assert.Equal(t, myErr, err)
assert.Nil(t, noJob)
assert.Equal(t, 1, callCount)
// Check job did not change in the DB.
j1Again, err := db.GetJobById(j1.Id)
assert.NoError(t, err)
testutils.AssertDeepEqual(t, j1, j1Again)
// Check no extra jobs in the DB.
jobs, err := db.GetJobsFromDateRange(begin, time.Now().Add(2*time.Nanosecond))
assert.NoError(t, err)
assert.Equal(t, 1, len(jobs))
assert.Equal(t, j1.Id, jobs[0].Id)
}
// Test UpdateJobWithRetries when retries are exhausted.
func testUpdateJobWithRetriesExhausted(t *testing.T, db JobDB) {
begin := time.Now()
// Create new job j1.
j1 := makeJob(begin.Add(time.Nanosecond))
assert.NoError(t, db.PutJob(j1))
// Update original.
j1.Status = JOB_STATUS_IN_PROGRESS
assert.NoError(t, db.PutJob(j1))
// Attempt update.
callCount := 0
noJob, err := UpdateJobWithRetries(db, j1.Id, func(job *Job) error {
callCount++
// Sneakily make an update in the background.
j1.Repo = "some-other-repo"
assert.NoError(t, db.PutJob(j1))
job.Status = JOB_STATUS_SUCCESS
return nil
})
assert.True(t, IsConcurrentUpdate(err))
assert.Equal(t, NUM_RETRIES, callCount)
assert.Nil(t, noJob)
// Check job did not change in the DB.
j1Again, err := db.GetJobById(j1.Id)
assert.NoError(t, err)
testutils.AssertDeepEqual(t, j1, j1Again)
// Check no extra jobs in the DB.
jobs, err := db.GetJobsFromDateRange(begin, time.Now().Add(2*time.Nanosecond))
assert.NoError(t, err)
assert.Equal(t, 1, len(jobs))
assert.Equal(t, j1.Id, jobs[0].Id)
}
// Test UpdateJobsWithRetries and UpdateJobWithRetries.
func TestUpdateJobsWithRetries(t *testing.T, db JobDB) {
testUpdateJobsWithRetriesSimple(t, db)
testUpdateJobsWithRetriesSuccess(t, db)
testUpdateJobsWithRetriesErrorInFunc(t, db)
testUpdateJobsWithRetriesErrorInPutJobs(t, db)
testUpdateJobsWithRetriesExhausted(t, db)
testUpdateJobWithRetriesSimple(t, db)
testUpdateJobWithRetriesSuccess(t, db)
testUpdateJobWithRetriesErrorInFunc(t, db)
testUpdateJobWithRetriesExhausted(t, db)
}
// makeTaskComment creates a comment with its ID fields based on the given repo,
// name, commit, and ts, and other fields based on n.
func makeTaskComment(n int, repo int, name int, commit int, ts time.Time) *TaskComment {
return &TaskComment{
Repo: fmt.Sprintf("r%d", repo),
Revision: fmt.Sprintf("c%d", commit),
Name: fmt.Sprintf("n%d", name),
Timestamp: ts,
TaskId: fmt.Sprintf("id%d", n),
User: fmt.Sprintf("u%d", n),
Message: fmt.Sprintf("m%d", n),
}
}
// makeTaskSpecComment creates a comment with its ID fields based on the given
// repo, name, and ts, and other fields based on n.
func makeTaskSpecComment(n int, repo int, name int, ts time.Time) *TaskSpecComment {
return &TaskSpecComment{
Repo: fmt.Sprintf("r%d", repo),
Name: fmt.Sprintf("n%d", name),
Timestamp: ts,
User: fmt.Sprintf("u%d", n),
Flaky: n%2 == 0,
IgnoreFailure: n>>1%2 == 0,
Message: fmt.Sprintf("m%d", n),
}
}
// makeCommitComment creates a comment with its ID fields based on the given
// repo, commit, and ts, and other fields based on n.
func makeCommitComment(n int, repo int, commit int, ts time.Time) *CommitComment {
return &CommitComment{
Repo: fmt.Sprintf("r%d", repo),
Revision: fmt.Sprintf("c%d", commit),
Timestamp: ts,
User: fmt.Sprintf("u%d", n),
IgnoreFailure: n>>1%2 == 0,
Message: fmt.Sprintf("m%d", n),
}
}
// TestCommentDB validates that db correctly implements the CommentDB interface.
func TestCommentDB(t *testing.T, db CommentDB) {
now := time.Now()
// Empty db.
{
actual, err := db.GetCommentsForRepos([]string{"r0", "r1", "r2"}, now.Add(-10000*time.Hour))
assert.NoError(t, err)
assert.Equal(t, 3, len(actual))
assert.Equal(t, "r0", actual[0].Repo)
assert.Equal(t, "r1", actual[1].Repo)
assert.Equal(t, "r2", actual[2].Repo)
for _, rc := range actual {
assert.Equal(t, 0, len(rc.TaskComments))
assert.Equal(t, 0, len(rc.TaskSpecComments))
assert.Equal(t, 0, len(rc.CommitComments))
}
}
// Add some comments.
tc1 := makeTaskComment(1, 1, 1, 1, now)
tc2 := makeTaskComment(2, 1, 1, 1, now.Add(2*time.Second))
tc3 := makeTaskComment(3, 1, 1, 1, now.Add(time.Second))
tc4 := makeTaskComment(4, 1, 1, 2, now)
tc5 := makeTaskComment(5, 1, 2, 2, now)
tc6 := makeTaskComment(6, 2, 3, 3, now)
tc6copy := tc6.Copy() // Adding identical comment should be ignored.
for _, c := range []*TaskComment{tc1, tc2, tc3, tc4, tc5, tc6, tc6copy} {
assert.NoError(t, db.PutTaskComment(c))
}
tc6.Message = "modifying after Put shouldn't affect stored comment"
sc1 := makeTaskSpecComment(1, 1, 1, now)
sc2 := makeTaskSpecComment(2, 1, 1, now.Add(2*time.Second))
sc3 := makeTaskSpecComment(3, 1, 1, now.Add(time.Second))
sc4 := makeTaskSpecComment(4, 1, 2, now)
sc5 := makeTaskSpecComment(5, 2, 3, now)
sc5copy := sc5.Copy() // Adding identical comment should be ignored.
for _, c := range []*TaskSpecComment{sc1, sc2, sc3, sc4, sc5, sc5copy} {
assert.NoError(t, db.PutTaskSpecComment(c))
}
sc5.Message = "modifying after Put shouldn't affect stored comment"
cc1 := makeCommitComment(1, 1, 1, now)
cc2 := makeCommitComment(2, 1, 1, now.Add(2*time.Second))
cc3 := makeCommitComment(3, 1, 1, now.Add(time.Second))
cc4 := makeCommitComment(4, 1, 2, now)
cc5 := makeCommitComment(5, 2, 3, now)
cc5copy := cc5.Copy() // Adding identical comment should be ignored.
for _, c := range []*CommitComment{cc1, cc2, cc3, cc4, cc5, cc5copy} {
assert.NoError(t, db.PutCommitComment(c))
}
cc5.Message = "modifying after Put shouldn't affect stored comment"
// Check that adding duplicate non-identical comment gives an error.
tc1different := tc1.Copy()
tc1different.Message = "not the same"
assert.True(t, IsAlreadyExists(db.PutTaskComment(tc1different)))
sc1different := sc1.Copy()
sc1different.Message = "not the same"
assert.True(t, IsAlreadyExists(db.PutTaskSpecComment(sc1different)))
cc1different := cc1.Copy()
cc1different.Message = "not the same"
assert.True(t, IsAlreadyExists(db.PutCommitComment(cc1different)))
expected := []*RepoComments{
{Repo: "r0"},
{
Repo: "r1",
TaskComments: map[string]map[string][]*TaskComment{
"c1": {
"n1": {tc1, tc3, tc2},
},
"c2": {
"n1": {tc4},
"n2": {tc5},
},
},
TaskSpecComments: map[string][]*TaskSpecComment{
"n1": {sc1, sc3, sc2},
"n2": {sc4},
},
CommitComments: map[string][]*CommitComment{
"c1": {cc1, cc3, cc2},
"c2": {cc4},
},
},
{
Repo: "r2",
TaskComments: map[string]map[string][]*TaskComment{
"c3": {
"n3": {tc6copy},
},
},
TaskSpecComments: map[string][]*TaskSpecComment{
"n3": {sc5copy},
},
CommitComments: map[string][]*CommitComment{
"c3": {cc5copy},
},
},
}
{
actual, err := db.GetCommentsForRepos([]string{"r0", "r1", "r2"}, now.Add(-10000*time.Hour))
assert.NoError(t, err)
testutils.AssertDeepEqual(t, expected, actual)
}
// Specifying a cutoff time shouldn't drop required comments.
{
actual, err := db.GetCommentsForRepos([]string{"r1"}, now.Add(time.Second))
assert.NoError(t, err)
assert.Equal(t, 1, len(actual))
{
tcs := actual[0].TaskComments["c1"]["n1"]
assert.True(t, len(tcs) >= 2)
offset := 0
if !tcs[0].Timestamp.Equal(tc3.Timestamp) {
offset = 1
}
testutils.AssertDeepEqual(t, tc3, tcs[offset])
testutils.AssertDeepEqual(t, tc2, tcs[offset+1])
}
{
scs := actual[0].TaskSpecComments["n1"]
assert.True(t, len(scs) >= 2)
offset := 0
if !scs[0].Timestamp.Equal(sc3.Timestamp) {
offset = 1
}
testutils.AssertDeepEqual(t, sc3, scs[offset])
testutils.AssertDeepEqual(t, sc2, scs[offset+1])
}
{
ccs := actual[0].CommitComments["c1"]
assert.True(t, len(ccs) >= 2)
offset := 0
if !ccs[0].Timestamp.Equal(cc3.Timestamp) {
offset = 1
}
testutils.AssertDeepEqual(t, cc3, ccs[offset])
testutils.AssertDeepEqual(t, cc2, ccs[offset+1])
}
}
// Delete some comments.
assert.NoError(t, db.DeleteTaskComment(tc3))
assert.NoError(t, db.DeleteTaskSpecComment(sc3))
assert.NoError(t, db.DeleteCommitComment(cc3))
// Delete should only look at the ID fields.
assert.NoError(t, db.DeleteTaskComment(tc1different))
assert.NoError(t, db.DeleteTaskSpecComment(sc1different))
assert.NoError(t, db.DeleteCommitComment(cc1different))
// Delete of nonexistent task should succeed.
assert.NoError(t, db.DeleteTaskComment(makeTaskComment(99, 1, 1, 1, now.Add(99*time.Second))))
assert.NoError(t, db.DeleteTaskComment(makeTaskComment(99, 1, 1, 99, now)))
assert.NoError(t, db.DeleteTaskComment(makeTaskComment(99, 1, 99, 1, now)))
assert.NoError(t, db.DeleteTaskComment(makeTaskComment(99, 99, 1, 1, now)))
assert.NoError(t, db.DeleteTaskSpecComment(makeTaskSpecComment(99, 1, 1, now.Add(99*time.Second))))
assert.NoError(t, db.DeleteTaskSpecComment(makeTaskSpecComment(99, 1, 99, now)))
assert.NoError(t, db.DeleteTaskSpecComment(makeTaskSpecComment(99, 99, 1, now)))
assert.NoError(t, db.DeleteCommitComment(makeCommitComment(99, 1, 1, now.Add(99*time.Second))))
assert.NoError(t, db.DeleteCommitComment(makeCommitComment(99, 1, 99, now)))
assert.NoError(t, db.DeleteCommitComment(makeCommitComment(99, 99, 1, now)))
expected[1].TaskComments["c1"]["n1"] = []*TaskComment{tc2}
expected[1].TaskSpecComments["n1"] = []*TaskSpecComment{sc2}
expected[1].CommitComments["c1"] = []*CommitComment{cc2}
{
actual, err := db.GetCommentsForRepos([]string{"r0", "r1", "r2"}, now.Add(-10000*time.Hour))
assert.NoError(t, err)
testutils.AssertDeepEqual(t, expected, actual)
}
// Delete all the comments.
for _, c := range []*TaskComment{tc2, tc4, tc5, tc6} {
assert.NoError(t, db.DeleteTaskComment(c))
}
for _, c := range []*TaskSpecComment{sc2, sc4, sc5} {
assert.NoError(t, db.DeleteTaskSpecComment(c))
}
for _, c := range []*CommitComment{cc2, cc4, cc5} {
assert.NoError(t, db.DeleteCommitComment(c))
}
{
actual, err := db.GetCommentsForRepos([]string{"r0", "r1", "r2"}, now.Add(-10000*time.Hour))
assert.NoError(t, err)
assert.Equal(t, 3, len(actual))
assert.Equal(t, "r0", actual[0].Repo)
assert.Equal(t, "r1", actual[1].Repo)
assert.Equal(t, "r2", actual[2].Repo)
for _, rc := range actual {
assert.Equal(t, 0, len(rc.TaskComments))
assert.Equal(t, 0, len(rc.TaskSpecComments))
assert.Equal(t, 0, len(rc.CommitComments))
}
}
}
func DummyGetRevisionTimestamp(ts time.Time) GetRevisionTimestamp {
return func(string, string) (time.Time, error) { return ts, nil }
}