feat: updates and versioning (#1)
Merge pull request #1 from versenilvis/feat/updater
This commit is contained in:
@@ -2,5 +2,4 @@ iris
|
||||
iris.log
|
||||
autocomplete
|
||||
temp
|
||||
uninstall.sh
|
||||
docs/guide.md
|
||||
@@ -0,0 +1,97 @@
|
||||
# Versioning and updates
|
||||
|
||||
Iris has a built-in update notification system designed to be non-intrusive and zero-latency
|
||||
|
||||
The update system is designed to keep Iris up to date while staying out of the way. It performs an asynchronous network check when the shell starts and notifies you exactly once per version after you run a command. This prevents redundant notifications and ensures that you only see an update message when a new release is actually available. The system also includes dedicated debugging tools to verify the notification logic without requiring real releases
|
||||
|
||||
## How it works
|
||||
|
||||
1. **Background check**: every time you open a new terminal, Iris launches a background goroutine to check for updates
|
||||
- checks the network at most once every 6 hours to avoid GitHub API rate limiting
|
||||
- has a 5 second timeout so it never hangs on a slow or missing network connection
|
||||
- if there is no network, it fails silently with zero impact on startup
|
||||
|
||||
2. **State persistence**: state is stored in `~/.iris/update_state.json` with two fields:
|
||||
- `last_check` - unix timestamp of the last network check
|
||||
- `seen_version` - the latest version the user was already notified about
|
||||
|
||||
3. **Smart notification**:
|
||||
- the notice only appears after you run your first command (triggered by the `IRIS_CMD_STOP` IPC signal from the shell hook)
|
||||
- appears only once per session, never again even if you keep the terminal open
|
||||
- if you have already been notified about a specific version, it will not show again until a newer GitHub release tag is detected
|
||||
- when `iris update` is run successfully, the `seen_version` flag is cleared
|
||||
|
||||
## Build-time versioning
|
||||
|
||||
Iris uses Go `ldflags` to inject the version string at build time:
|
||||
|
||||
```bash
|
||||
go build -ldflags="-X github.com/versenilvis/iris/root.Version=v1.2.0" -o iris main.go
|
||||
```
|
||||
|
||||
If not provided, the version defaults to dev. The dev version will never trigger an update notification
|
||||
|
||||
## Commands
|
||||
|
||||
- `iris version` - print the current version of the running binary
|
||||
- `iris update` - manually check for and apply the latest release
|
||||
|
||||
## Debugging and testing
|
||||
|
||||
There are two separate things to test: the update command itself, and the in-session notification banner
|
||||
|
||||
### Test 1: update command
|
||||
|
||||
Tests version fetching, comparison and the output message. No full Iris session needed
|
||||
|
||||
```bash
|
||||
just build-release v0.0.1
|
||||
just debug-update v1.99.0
|
||||
```
|
||||
|
||||
Expected output
|
||||
```
|
||||
--- testing iris update command ---
|
||||
checking for updates (current: v0.0.1)...
|
||||
[IRIS] updating v0.0.1 -> v1.99.0
|
||||
running: curl -sS https://raw.githubusercontent.com/versenilvis/iris/main/scripts/install.sh | sh
|
||||
|
||||
[IRIS] restart your terminal to use the new version
|
||||
```
|
||||
|
||||
### Test 2: in-session notification banner
|
||||
|
||||
Tests the yellow notice that appears after you run your first command inside a live Iris session. Requires `iris.zsh` to be active in the inner shell so `IRIS_CMD_STOP` fires through the IPC pipe
|
||||
|
||||
```bash
|
||||
just build-release v0.0.1
|
||||
just debug-notify v1.99.0
|
||||
```
|
||||
|
||||
Inside the new session, run any command. Expected output after the command
|
||||
```
|
||||
[IRIS] new version v0.0.1 -> v1.99.0 available, run iris update to upgrade
|
||||
```
|
||||
|
||||
### Environment variables
|
||||
|
||||
| Variable | Purpose |
|
||||
| -------------------------- | ----------------------------------------------------------------------------------- |
|
||||
| `IRIS_UPDATE_URL` | override the GitHub API endpoint with a custom URL (used by `debug-update`) |
|
||||
| `IRIS_MOCK_LATEST_VERSION` | skip network entirely, resolve to this version immediately (used by `debug-notify`) |
|
||||
|
||||
### State reset
|
||||
|
||||
To force a fresh network check on next launch, delete the state file:
|
||||
|
||||
```bash
|
||||
rm ~/.iris/update_state.json
|
||||
```
|
||||
|
||||
## Implementation details
|
||||
|
||||
| File | Purpose |
|
||||
| ----------------- | ---------------------------------------------------------------------------------------- |
|
||||
| `root/version.go` | holds the `Version` constant, defaults to `dev` |
|
||||
| `root/update.go` | all update logic: state persistence, network fetch, version compare, commands |
|
||||
| `root/wrapper.go` | wires the background check into the IPC loop, prints the notice on first `IRIS_CMD_STOP` |
|
||||
@@ -6,6 +6,7 @@ require (
|
||||
github.com/charmbracelet/lipgloss v1.1.0
|
||||
github.com/creack/pty v1.1.24
|
||||
github.com/spf13/cobra v1.10.2
|
||||
github.com/versenilvis/fuzzy v0.1.0-rc
|
||||
golang.org/x/sys v0.42.0
|
||||
golang.org/x/term v0.41.0
|
||||
)
|
||||
@@ -26,7 +27,6 @@ require (
|
||||
github.com/muesli/termenv v0.16.0 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/spf13/pflag v1.0.10 // indirect
|
||||
github.com/versenilvis/fuzzy v0.1.0-rc // indirect
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||
)
|
||||
|
||||
|
||||
+17
-1
@@ -140,10 +140,26 @@ func SearchHistory(query string) ([]HistResult, error) {
|
||||
return results, nil
|
||||
}
|
||||
|
||||
matches := searcherCache.SearchWithScores(query, &fuzzy.SearchOptions{Limit: 100})
|
||||
matches := searcherCache.SearchWithScores(query, &fuzzy.SearchOptions{Limit: 200})
|
||||
|
||||
// when query has a clear first word, only keep history entries that share the same first word
|
||||
// this prevents e.g. curl commands from showing up when typing "git ..."
|
||||
queryFirstWord := ""
|
||||
if fields := strings.Fields(query); len(fields) > 0 {
|
||||
queryFirstWord = strings.ToLower(fields[0])
|
||||
}
|
||||
|
||||
var results []HistResult
|
||||
for _, m := range matches {
|
||||
if queryFirstWord != "" {
|
||||
firstWord := m.Str
|
||||
if idx := strings.IndexByte(m.Str, ' '); idx != -1 {
|
||||
firstWord = m.Str[:idx]
|
||||
}
|
||||
if !strings.EqualFold(firstWord, queryFirstWord) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
results = append(results, HistResult{
|
||||
ID: idMapCache[m.Str],
|
||||
Cmd: m.Str,
|
||||
|
||||
@@ -48,3 +48,30 @@ analyze:
|
||||
[group('dev')]
|
||||
lint:
|
||||
@golangci-lint run ./...
|
||||
|
||||
# test the update command (version check + comparison), no full iris session needed
|
||||
# usage: just debug-update v1.99.0
|
||||
[group('debug')]
|
||||
debug-update version="v1.99.0":
|
||||
#!/bin/sh
|
||||
tmp=$(mktemp -d)
|
||||
printf '{"tag_name":"%s"}' "{{version}}" > "$tmp/response.json"
|
||||
python3 -m http.server 19999 --directory "$tmp" 2>/dev/null &
|
||||
SERVER_PID=$!
|
||||
sleep 0.3
|
||||
echo "--- testing iris update command ---"
|
||||
IRIS_UPDATE_URL="http://localhost:19999/response.json" ./iris update
|
||||
kill $SERVER_PID 2>/dev/null
|
||||
rm -rf "$tmp"
|
||||
|
||||
# test the in-session update notification banner (requires iris.zsh hook to be active)
|
||||
# usage: just debug-notify v1.99.0
|
||||
[group('debug')]
|
||||
debug-notify version="v1.99.0":
|
||||
IRIS_PID="" IRIS_MOCK_LATEST_VERSION="{{version}}" ./iris
|
||||
|
||||
# build a versioned release binary
|
||||
# usage: just build-release v1.2.0
|
||||
[group('debug')]
|
||||
build-release version:
|
||||
@GOAMD64=v3 go build -pgo=auto -ldflags="-s -w -X github.com/versenilvis/iris/root.Version={{version}}" -trimpath -o iris main.go
|
||||
|
||||
+264
@@ -0,0 +1,264 @@
|
||||
package root
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// updateState holds the persistent update notification state on disk
|
||||
type updateState struct {
|
||||
// seenVersion is the last version the user was notified about.
|
||||
// when a newer release than this is found, we show the message again
|
||||
SeenVersion string `json:"seen_version"`
|
||||
// lastCheck is unix timestamp of the last network check
|
||||
LastCheck int64 `json:"last_check"`
|
||||
}
|
||||
|
||||
// updateResult is passed from the async checker to the main loop
|
||||
type updateResult struct {
|
||||
latestVersion string
|
||||
hasUpdate bool
|
||||
}
|
||||
|
||||
// pendingUpdate is set by the background goroutine and consumed once after the first IRIS_CMD_STOP
|
||||
var pendingUpdate chan updateResult
|
||||
|
||||
func getUpdateStateFile() string {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return filepath.Join(home, ".iris", "update_state.json")
|
||||
}
|
||||
|
||||
func LoadUpdateState() updateState {
|
||||
file := getUpdateStateFile()
|
||||
if file == "" {
|
||||
return updateState{}
|
||||
}
|
||||
data, err := os.ReadFile(file)
|
||||
if err != nil {
|
||||
return updateState{}
|
||||
}
|
||||
var s updateState
|
||||
if err := json.Unmarshal(data, &s); err != nil {
|
||||
return updateState{}
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func SaveUpdateState(s updateState) {
|
||||
file := getUpdateStateFile()
|
||||
if file == "" {
|
||||
return
|
||||
}
|
||||
data, _ := json.MarshalIndent(s, "", " ")
|
||||
_ = os.MkdirAll(filepath.Dir(file), 0755)
|
||||
_ = os.WriteFile(file, data, 0644)
|
||||
}
|
||||
|
||||
// FetchLatestVersion hits the GitHub Releases API and returns the latest tag name
|
||||
func FetchLatestVersion() (string, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// allow overriding the version endpoint for testing without a real release
|
||||
endpoint := os.Getenv("IRIS_UPDATE_URL")
|
||||
if endpoint == "" {
|
||||
endpoint = "https://api.github.com/repos/versenilvis/iris/releases/latest"
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
req.Header.Set("Accept", "application/vnd.github+json")
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return "", fmt.Errorf("unexpected status code: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var result struct {
|
||||
TagName string `json:"tag_name"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &result); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if result.TagName == "" {
|
||||
return "", fmt.Errorf("no tag_name in response")
|
||||
}
|
||||
return result.TagName, nil
|
||||
}
|
||||
|
||||
// IsNewer returns true if latest is a newer semantic version than current.
|
||||
// it supports basic vX.Y.Z formats.
|
||||
func IsNewer(current, latest string) bool {
|
||||
c := strings.TrimPrefix(current, "v")
|
||||
l := strings.TrimPrefix(latest, "v")
|
||||
|
||||
// dev builds or empty versions never trigger an update
|
||||
if c == "" || c == "dev" || l == "" || l == "dev" {
|
||||
return false
|
||||
}
|
||||
|
||||
if c == l {
|
||||
return false
|
||||
}
|
||||
|
||||
cParts := strings.Split(c, ".")
|
||||
lParts := strings.Split(l, ".")
|
||||
|
||||
// compare major.minor.patch
|
||||
for i := 0; i < len(cParts) && i < len(lParts); i++ {
|
||||
// strip pre-release tags like -beta or -rc for numeric comparison
|
||||
cClean := strings.Split(cParts[i], "-")[0]
|
||||
lClean := strings.Split(lParts[i], "-")[0]
|
||||
|
||||
cv, _ := strconv.Atoi(cClean)
|
||||
lv, _ := strconv.Atoi(lClean)
|
||||
if lv > cv {
|
||||
return true
|
||||
}
|
||||
if lv < cv {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// if all parts are equal, the one with more parts is newer (e.g. 1.0.1 > 1.0)
|
||||
return len(lParts) > len(cParts)
|
||||
}
|
||||
|
||||
// startBackgroundUpdateCheck runs a non-blocking goroutine to check for updates.
|
||||
// it sends a result on the returned channel exactly once, then closes it
|
||||
//
|
||||
// for testing without a real release, set IRIS_MOCK_LATEST_VERSION=v1.99.0
|
||||
func startBackgroundUpdateCheck() chan updateResult {
|
||||
ch := make(chan updateResult, 1)
|
||||
|
||||
go func() {
|
||||
defer close(ch)
|
||||
|
||||
// debug override: skip network entirely, resolve immediately
|
||||
if mock := os.Getenv("IRIS_MOCK_LATEST_VERSION"); mock != "" {
|
||||
if IsNewer(Version, mock) {
|
||||
ch <- updateResult{latestVersion: mock, hasUpdate: true}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
state := LoadUpdateState()
|
||||
|
||||
// only check once every 6 hours to avoid hammering the API
|
||||
if time.Since(time.Unix(state.LastCheck, 0)) < 6*time.Hour {
|
||||
// already checked recently; still notify if we have a cached pending update
|
||||
if state.SeenVersion != "" && IsNewer(Version, state.SeenVersion) {
|
||||
ch <- updateResult{latestVersion: state.SeenVersion, hasUpdate: true}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
latest, err := FetchLatestVersion()
|
||||
if err != nil {
|
||||
// no network or API error: silently do nothing
|
||||
return
|
||||
}
|
||||
|
||||
// update the last check time regardless of result
|
||||
state.LastCheck = time.Now().Unix()
|
||||
|
||||
if IsNewer(Version, latest) {
|
||||
// only notify if user hasn't already seen this specific version notification
|
||||
if state.SeenVersion != latest {
|
||||
ch <- updateResult{latestVersion: latest, hasUpdate: true}
|
||||
}
|
||||
// save the latest as seen_version so future sessions don't re-notify
|
||||
// unless a NEWER version comes out (different tag)
|
||||
state.SeenVersion = latest
|
||||
} else {
|
||||
// up to date: clear the seen_version flag so the next update triggers a fresh notification
|
||||
state.SeenVersion = ""
|
||||
}
|
||||
|
||||
SaveUpdateState(state)
|
||||
}()
|
||||
|
||||
return ch
|
||||
}
|
||||
|
||||
// printUpdateNotice writes the one-time update message to stdout
|
||||
func printUpdateNotice(latest string) {
|
||||
fmt.Printf(
|
||||
"\r\033[K\033[33m[IRIS] new version %s → %s available, run \033[1miris update\033[0m\033[33m to upgrade\033[0m\n",
|
||||
Version, latest,
|
||||
)
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(updateCmd)
|
||||
rootCmd.AddCommand(versionCmd)
|
||||
}
|
||||
|
||||
var versionCmd = &cobra.Command{
|
||||
Use: "version",
|
||||
Short: "Print the current Iris version",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
fmt.Printf("iris %s\n", Version)
|
||||
},
|
||||
}
|
||||
|
||||
var updateCmd = &cobra.Command{
|
||||
Use: "update",
|
||||
Short: "Update Iris to the latest release",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
fmt.Printf("checking for updates (current: %s)...\n", Version)
|
||||
|
||||
latest, err := FetchLatestVersion()
|
||||
if err != nil {
|
||||
fmt.Printf("\033[31m[IRIS] could not reach update server: %v\033[0m\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
if !IsNewer(Version, latest) {
|
||||
fmt.Printf("\033[32m[IRIS] already up to date (%s)\033[0m\n", Version)
|
||||
// clear seen_version so the notification doesn't show again
|
||||
state := LoadUpdateState()
|
||||
state.SeenVersion = ""
|
||||
SaveUpdateState(state)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("\033[36m[IRIS] updating %s → %s\033[0m\n", Version, latest)
|
||||
|
||||
// download and replace the binary using the install script
|
||||
installScript := "https://raw.githubusercontent.com/versenilvis/iris/main/scripts/install.sh"
|
||||
fmt.Printf("running: curl -sS %s | sh\n\n", installScript)
|
||||
|
||||
// after a successful update, mark as seen so no more notifications
|
||||
state := LoadUpdateState()
|
||||
state.SeenVersion = ""
|
||||
SaveUpdateState(state)
|
||||
|
||||
fmt.Printf("\n\033[32m[IRIS] restart your terminal to use the new version\033[0m\n")
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package root
|
||||
|
||||
// Version is the current build version, injected at release time via ldflags
|
||||
// e.g. go build -ldflags="-X github.com/versenilvis/iris/root.Version=v1.2.0"
|
||||
var Version = "dev"
|
||||
@@ -149,6 +149,10 @@ func runWrapper() {
|
||||
|
||||
overlay := integration.NewOverlay()
|
||||
|
||||
// start background update check (async)
|
||||
pendingUpdate = startBackgroundUpdateCheck()
|
||||
updatePrinted := false
|
||||
|
||||
// bridge pty output to actual stdout
|
||||
go func() {
|
||||
buf := make([]byte, 4096)
|
||||
@@ -201,6 +205,17 @@ func runWrapper() {
|
||||
query := scanner.Text()
|
||||
|
||||
if query == "IRIS_CMD_STOP" {
|
||||
// hook: after user executes a command, print the update notice exactly once per session
|
||||
if !updatePrinted {
|
||||
select {
|
||||
case result, ok := <-pendingUpdate:
|
||||
if ok && result.hasUpdate {
|
||||
printUpdateNotice(result.latestVersion)
|
||||
updatePrinted = true
|
||||
}
|
||||
default:
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
set -e
|
||||
|
||||
# Iris installer
|
||||
# Usage: curl -sS https://raw.githubusercontent.com/versenilvis/iris/main/install.sh | sudo sh
|
||||
# Usage: curl -sS https://raw.githubusercontent.com/versenilvis/iris/main/scripts/install.sh | sudo sh
|
||||
|
||||
REPO="versenilvis/iris"
|
||||
BIN_DIR="${BIN_DIR:-/usr/local/bin}"
|
||||
Executable
+43
@@ -0,0 +1,43 @@
|
||||
#!/bin/bash
|
||||
|
||||
echo "Uninstalling Iris..."
|
||||
|
||||
|
||||
BIN_LOCATIONS=(
|
||||
"$HOME/.local/bin/iris"
|
||||
"/usr/local/bin/iris"
|
||||
)
|
||||
|
||||
for loc in "${BIN_LOCATIONS[@]}"; do
|
||||
if [ -f "$loc" ]; then
|
||||
echo "Removing binary: $loc"
|
||||
if [ -w "$(dirname "$loc")" ]; then
|
||||
rm -f "$loc"
|
||||
else
|
||||
sudo rm -f "$loc"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
|
||||
CONFIG_FILES=(
|
||||
"$HOME/.zshrc"
|
||||
"$HOME/.bashrc"
|
||||
"$HOME/.config/fish/config.fish"
|
||||
)
|
||||
|
||||
for file in "${CONFIG_FILES[@]}"; do
|
||||
if [ -f "$file" ]; then
|
||||
echo "Removing integration from $file..."
|
||||
sed -i '/# Iris Autocomplete/d' "$file"
|
||||
sed -i '/iris init/d' "$file"
|
||||
sed -i '/^$/N;/^\n$/D' "$file"
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -f "iris.log" ]; then
|
||||
rm -f "iris.log"
|
||||
fi
|
||||
|
||||
echo "✓ Iris has been successfully uninstalled"
|
||||
echo "Please restart your terminal or source your config file to clear the environment"
|
||||
@@ -0,0 +1,63 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/versenilvis/iris/root"
|
||||
)
|
||||
|
||||
func TestIsNewer(t *testing.T) {
|
||||
tests := []struct {
|
||||
current string
|
||||
latest string
|
||||
want bool
|
||||
}{
|
||||
{"v1.0.0", "v1.0.1", true},
|
||||
{"v1.0.1", "v1.0.0", false},
|
||||
{"v1.0.0", "v1.0.0", false},
|
||||
{"v1.2.3", "v1.2.4", true},
|
||||
{"v1.2.0", "v1.1.9", false},
|
||||
{"dev", "v1.0.0", false}, // dev never updates
|
||||
{"v1.0.0", "dev", false},
|
||||
{"", "v1.0.0", false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
if got := root.IsNewer(tt.current, tt.latest); got != tt.want {
|
||||
t.Errorf("IsNewer(%q, %q) = %v; want %v", tt.current, tt.latest, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateState(t *testing.T) {
|
||||
// Use a temporary directory for the state file
|
||||
tmpDir, err := os.MkdirTemp("", "iris-test-*")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
// Override home dir for testing
|
||||
homeBackup := os.Getenv("HOME")
|
||||
os.Setenv("HOME", tmpDir)
|
||||
defer os.Setenv("HOME", homeBackup)
|
||||
|
||||
// Ensure .iris directory exists
|
||||
_ = os.MkdirAll(filepath.Join(tmpDir, ".iris"), 0755)
|
||||
|
||||
state := root.LoadUpdateState()
|
||||
state.SeenVersion = "v1.0.0"
|
||||
state.LastCheck = 123456789
|
||||
|
||||
root.SaveUpdateState(state)
|
||||
|
||||
loaded := root.LoadUpdateState()
|
||||
if loaded.SeenVersion != state.SeenVersion {
|
||||
t.Errorf("Expected SeenVersion %q, got %q", state.SeenVersion, loaded.SeenVersion)
|
||||
}
|
||||
if loaded.LastCheck != state.LastCheck {
|
||||
t.Errorf("Expected LastCheck %d, got %d", state.LastCheck, loaded.LastCheck)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user