Files
comic-hasher/storage_test.go

515 lines
16 KiB
Go

package main_test
import (
"bytes"
"crypto"
"fmt"
"io"
"net/http/httptest"
"path/filepath"
"runtime"
"strings"
"testing"
gzip "github.com/klauspost/pgzip"
gcmp "cmp"
"gitea.narnian.us/lordwelch/comic-hasher/ch"
"gitea.narnian.us/lordwelch/comic-hasher/log"
"gitea.narnian.us/lordwelch/comic-hasher/storage"
"gitea.narnian.us/lordwelch/goimagehash"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
)
// func init() { log.LogLevels[log.ALL] = -99 }
// Keep in-line with savedHashes_test.go
var cmpOpts = []cmp.Option{
cmpopts.EquateEmpty(),
cmpopts.SortSlices(func(a ch.Result, b ch.Result) int {
return gcmp.Or(
gcmp.Compare(a.Hash.Kind, b.Hash.Kind),
gcmp.Compare(a.Hash.Hash, b.Hash.Hash),
a.ID.Compare(b.ID),
)
}),
}
var (
sample1SavedHashes = []ch.SavedHashV2{
{ID: ch.ID{ID: "sample1.jpg", Domain: ch.NewSource("example.org")}, Hash: ch.Hash{Kind: goimagehash.AHash, Hash: 0x7eff3f030703c1fc}},
{ID: ch.ID{ID: "sample1.jpg", Domain: ch.NewSource("example.org")}, Hash: ch.Hash{Kind: goimagehash.DHash, Hash: 0xe89f6f63af4f8b69}},
{ID: ch.ID{ID: "sample1.jpg", Domain: ch.NewSource("example.org")}, Hash: ch.Hash{Kind: goimagehash.PHash, Hash: 0xaf95d2205c7b1f82}},
}
sample2SavedHashes = []ch.SavedHashV2{
{ID: ch.ID{ID: "sample2.jpg", Domain: ch.NewSource("example.org")}, Hash: ch.Hash{Kind: goimagehash.AHash, Hash: 0x003c7e7e7e7e3c18}},
{ID: ch.ID{ID: "sample2.jpg", Domain: ch.NewSource("example.org")}, Hash: ch.Hash{Kind: goimagehash.DHash, Hash: 0x71f0f4d2f0b8f071}},
{ID: ch.ID{ID: "sample2.jpg", Domain: ch.NewSource("example.org")}, Hash: ch.Hash{Kind: goimagehash.PHash, Hash: 0x95274a996be4349e}},
}
sample3SavedHashes = []ch.SavedHashV2{
{ID: ch.ID{ID: "sample3.jpg", Domain: ch.NewSource("example.org")}, Hash: ch.Hash{Kind: goimagehash.AHash, Hash: 0x7eff3f030703c1fc}},
{ID: ch.ID{ID: "sample3.jpg", Domain: ch.NewSource("example.org")}, Hash: ch.Hash{Kind: goimagehash.DHash, Hash: 0xe89f6f63af4f8bc9}},
{ID: ch.ID{ID: "sample3.jpg", Domain: ch.NewSource("example.org")}, Hash: ch.Hash{Kind: goimagehash.PHash, Hash: 0xaf95d2205c7b1f82}},
}
sample4SavedHashes = []ch.SavedHashV2{
{ID: ch.ID{ID: "sample4.jpg", Domain: ch.NewSource("example.org")}, Hash: ch.Hash{Kind: goimagehash.AHash, Hash: 0x183c3e3e7e2e3c18}},
{ID: ch.ID{ID: "sample4.jpg", Domain: ch.NewSource("example.org")}, Hash: ch.Hash{Kind: goimagehash.DHash, Hash: 0x716cd4e8dccc78b1}},
{ID: ch.ID{ID: "sample4.jpg", Domain: ch.NewSource("example.org")}, Hash: ch.Hash{Kind: goimagehash.PHash, Hash: 0x979c5a9369cd6518}},
}
exampleSavedHashes = []ch.SavedHashV2{
{ID: ch.ID{ID: "example.png", Domain: ch.NewSource("example.org")}, Hash: ch.Hash{Kind: goimagehash.AHash, Hash: 0x246e7e3c1c181800}},
{ID: ch.ID{ID: "example.png", Domain: ch.NewSource("example.org")}, Hash: ch.Hash{Kind: goimagehash.DHash, Hash: 0xccccccf139b0b210}},
{ID: ch.ID{ID: "example.png", Domain: ch.NewSource("example.org")}, Hash: ch.Hash{Kind: goimagehash.PHash, Hash: 0xd2f2361e2f496368}},
}
)
func SeedHashes(hs ch.HashStorage) {
hashes := []ch.SavedHashV2{}
hashes = append(hashes, sample1SavedHashes...)
hashes = append(hashes, sample2SavedHashes...)
hashes = append(hashes, sample3SavedHashes...)
hashes = append(hashes, sample4SavedHashes...)
hashes = append(hashes, exampleSavedHashes...)
savedHashes, err := ch.NewSavedHashesV3(&ch.SavedHashesV2{Hashes: hashes})
if err != nil {
panic(err)
}
hs.DecodeHashes(savedHashes)
}
func GetMatchesBasic(t *testing.T, hs ch.HashStorage) {
results, err := hs.GetMatches([]ch.Hash{{Hash: 0x183c3e3e7e2e3c18, Kind: goimagehash.AHash}}, 8, true)
if err != nil {
t.Fatal("Failed to search hashes:", err)
}
expected := []ch.Result{
{
Hash: ch.Hash{Hash: 0x183c3e3e7e2e3c18, Kind: 1},
ID: ch.ID{Domain: ch.NewSource("example.org"), ID: "sample4.jpg"},
Distance: 0,
EquivalentIDs: []ch.ID{{Domain: ch.NewSource("example.org"), ID: "sample4.jpg"}},
},
}
if diff := cmp.Diff(expected, results, cmpOpts...); diff != "" {
t.Errorf("GetMatches() mismatch (-want +got):\n%s", diff)
}
}
//////////// Generated
func GetMatchesNoMatchExact(t *testing.T, hs ch.HashStorage) {
results, err := hs.GetMatches([]ch.Hash{{Hash: 0xffffffffffffffff, Kind: goimagehash.AHash}}, 8, true)
if err != nil {
t.Fatal("Failed to search hashes:", err)
}
expected := []ch.Result{}
if diff := cmp.Diff(expected, results, cmpOpts...); diff != "" {
t.Errorf("GetMatches() mismatch (-want +got):\n%s", diff)
}
}
func GetMatchesSharedAHash(t *testing.T, hs ch.HashStorage) {
// sample1 and sample3 share the same AHash
results, err := hs.GetMatches([]ch.Hash{{Hash: 0x7eff3f030703c1fc, Kind: goimagehash.AHash}}, 8, true)
if err != nil {
t.Fatal("Failed to search hashes:", err)
}
expected := []ch.Result{
{
Hash: ch.Hash{Hash: 0x7eff3f030703c1fc, Kind: goimagehash.AHash},
ID: ch.ID{Domain: ch.NewSource("example.org"), ID: "sample1.jpg"},
Distance: 0,
EquivalentIDs: []ch.ID{{Domain: ch.NewSource("example.org"), ID: "sample1.jpg"}},
},
{
Hash: ch.Hash{Hash: 0x7eff3f030703c1fc, Kind: goimagehash.AHash},
ID: ch.ID{Domain: ch.NewSource("example.org"), ID: "sample3.jpg"},
Distance: 0,
EquivalentIDs: []ch.ID{{Domain: ch.NewSource("example.org"), ID: "sample3.jpg"}},
},
}
if diff := cmp.Diff(expected, results, cmpOpts...); diff != "" {
t.Errorf("GetMatches() mismatch (-want +got):\n%s", diff)
}
}
func GetMatchesNearDHash(t *testing.T, hs ch.HashStorage) {
results, err := hs.GetMatches([]ch.Hash{{Hash: 0xe89f6f63af4f8b59, Kind: goimagehash.DHash}}, 8, true)
if err != nil {
t.Fatal("Failed to search hashes:", err)
}
expected := []ch.Result{
{
Hash: ch.Hash{Hash: 0xe89f6f63af4f8b69, Kind: goimagehash.DHash},
ID: ch.ID{Domain: ch.NewSource("example.org"), ID: "sample1.jpg"},
Distance: 2,
EquivalentIDs: []ch.ID{{Domain: ch.NewSource("example.org"), ID: "sample1.jpg"}},
},
{
Hash: ch.Hash{Hash: 0xe89f6f63af4f8bc9, Kind: goimagehash.DHash},
ID: ch.ID{Domain: ch.NewSource("example.org"), ID: "sample3.jpg"},
Distance: 2,
EquivalentIDs: []ch.ID{{Domain: ch.NewSource("example.org"), ID: "sample3.jpg"}},
},
}
if diff := cmp.Diff(expected, results, cmpOpts...); diff != "" {
t.Errorf("GetMatches() mismatch (-want +got):\n%s", diff)
}
}
func GetMatchesMultipleHashes(t *testing.T, hs ch.HashStorage) {
results, err := hs.GetMatches([]ch.Hash{
{Hash: 0x003c7e7e7e7e3c18, Kind: goimagehash.AHash},
{Hash: 0x71f0f4d2f0b8f071, Kind: goimagehash.DHash},
{Hash: 0x95274a996be4349e, Kind: goimagehash.PHash},
}, 8, true)
if err != nil {
t.Fatal("Failed to search hashes:", err)
}
expected := []ch.Result{
{
Hash: ch.Hash{Hash: 0x003c7e7e7e7e3c18, Kind: goimagehash.AHash},
ID: ch.ID{Domain: ch.NewSource("example.org"), ID: "sample2.jpg"},
Distance: 0,
EquivalentIDs: []ch.ID{{Domain: ch.NewSource("example.org"), ID: "sample2.jpg"}},
},
{
Hash: ch.Hash{Hash: 0x71f0f4d2f0b8f071, Kind: goimagehash.DHash},
ID: ch.ID{Domain: ch.NewSource("example.org"), ID: "sample2.jpg"},
Distance: 0,
EquivalentIDs: []ch.ID{{Domain: ch.NewSource("example.org"), ID: "sample2.jpg"}},
},
{
Hash: ch.Hash{Hash: 0x95274a996be4349e, Kind: goimagehash.PHash},
ID: ch.ID{Domain: ch.NewSource("example.org"), ID: "sample2.jpg"},
Distance: 0,
EquivalentIDs: []ch.ID{{Domain: ch.NewSource("example.org"), ID: "sample2.jpg"}},
},
}
if diff := cmp.Diff(expected, results, cmpOpts...); diff != "" {
t.Errorf("GetMatches() mismatch (-want +got):\n%s", diff)
}
}
func GetIDs(t *testing.T, hs ch.HashStorage) {
results := hs.GetIDs(ch.ID{Domain: ch.NewSource("example.org"), ID: "sample4.jpg"})
expected := []ch.ID{
{Domain: ch.NewSource("example.org"), ID: "sample4.jpg"},
}
if diff := cmp.Diff(expected, results, cmpOpts...); diff != "" {
t.Errorf("GetIDs() mismatch (-want +got):\n%s", diff)
}
}
func TestBasicStorage(t *testing.T) {
hs, err := storage.NewBasicStorage()
if err != nil {
t.Fatal("Failed to create basic storage:", err)
}
SeedHashes(hs)
t.Run("GetMatchesBasic", func(t *testing.T) { GetMatchesBasic(t, hs) })
t.Run("GetMatchesNoMatchExact", func(t *testing.T) { GetMatchesNoMatchExact(t, hs) })
t.Run("GetMatchesSharedAHash", func(t *testing.T) { GetMatchesSharedAHash(t, hs) })
t.Run("GetMatchesNearDHash", func(t *testing.T) { GetMatchesNearDHash(t, hs) })
t.Run("GetMatchesMultipleHashes", func(t *testing.T) { GetMatchesMultipleHashes(t, hs) })
t.Run("GetIDs", func(t *testing.T) { GetIDs(t, hs) })
}
func TestMapStorage(t *testing.T) {
hs, err := storage.NewMapStorage()
if err != nil {
t.Fatal("Failed to create map storage:", err)
}
SeedHashes(hs)
t.Run("GetMatchesBasic", func(t *testing.T) { GetMatchesBasic(t, hs) })
t.Run("GetMatchesNoMatchExact", func(t *testing.T) { GetMatchesNoMatchExact(t, hs) })
t.Run("GetMatchesSharedAHash", func(t *testing.T) { GetMatchesSharedAHash(t, hs) })
t.Run("GetMatchesNearDHash", func(t *testing.T) { GetMatchesNearDHash(t, hs) })
t.Run("GetMatchesMultipleHashes", func(t *testing.T) { GetMatchesMultipleHashes(t, hs) })
t.Run("GetIDs", func(t *testing.T) { GetIDs(t, hs) })
}
func TestVPSStorage(t *testing.T) {
hs, err := storage.NewVPStorage()
if err != nil {
t.Fatal("Failed to create basic storage:", err)
}
SeedHashes(hs)
t.Run("GetMatchesBasic", func(t *testing.T) { GetMatchesBasic(t, hs) })
t.Run("GetMatchesNoMatchExact", func(t *testing.T) { GetMatchesNoMatchExact(t, hs) })
t.Run("GetMatchesSharedAHash", func(t *testing.T) { GetMatchesSharedAHash(t, hs) })
t.Run("GetMatchesNearDHash", func(t *testing.T) { GetMatchesNearDHash(t, hs) })
t.Run("GetMatchesMultipleHashes", func(t *testing.T) { GetMatchesMultipleHashes(t, hs) })
t.Run("GetIDs", func(t *testing.T) { GetIDs(t, hs) })
}
func TestSqliteStorage(t *testing.T) {
hs, err := storage.NewSqliteStorage("sqlite", filepath.Join(t.TempDir(), "sqlite.db"))
if err != nil {
t.Fatal("Failed to create sqlite storage:", err)
}
SeedHashes(hs)
t.Run("GetMatchesBasic", func(t *testing.T) { GetMatchesBasic(t, hs) })
t.Run("GetMatchesNoMatchExact", func(t *testing.T) { GetMatchesNoMatchExact(t, hs) })
t.Run("GetMatchesSharedAHash", func(t *testing.T) { GetMatchesSharedAHash(t, hs) })
t.Run("GetMatchesNearDHash", func(t *testing.T) { GetMatchesNearDHash(t, hs) })
t.Run("GetMatchesMultipleHashes", func(t *testing.T) { GetMatchesMultipleHashes(t, hs) })
t.Run("GetIDs", func(t *testing.T) { GetIDs(t, hs) })
}
func TestHashServer(t *testing.T) {
hs, err := storage.NewBasicStorage()
if err != nil {
t.Fatal("Failed to create basic storage:", err)
}
SeedHashes(hs)
HashServer := ch.NewHashServer(filepath.Join(t.TempDir(), "hashes.gz"), true)
req := httptest.NewRequest("GET", "/", nil)
err = HashServer.WriteHashes(hs)
if err != nil {
t.Fatal("failed to write hashes:", err)
}
recorder := httptest.NewRecorder()
HashServer.DownloadHashes(recorder, req)
gz, err := gzip.NewReader(recorder.Body)
if err != nil {
t.Fatal("HashServer did not send a gzipped response:", err)
}
decompressedHashes, err := io.ReadAll(gz)
if err != nil {
t.Fatal("Failed to read response:", err)
}
_, err = ch.DecodeHashesV3(bytes.NewReader(decompressedHashes))
if err != nil {
t.Fatal("failed to validate hashes:", err)
}
var md5 []byte
for digest := range strings.SplitSeq(recorder.Header().Get("Repr-Digest"), ",") {
kind, _ := log.Partition(digest, "=:")
if kind != "MD5" {
continue
}
md5 = crypto.MD5.New().Sum(recorder.Body.Bytes())
}
expected := "d41d8cd98f00b204e9800998ecf8427e"
if diff := cmp.Diff(expected, fmt.Sprintf("%x", md5), cmpOpts...); diff != "" {
t.Errorf("GetMatches() mismatch (-want +got):\n%s", diff)
}
}
func TestLoadEmbeddedHashes(t *testing.T) {
hs, err := storage.NewBasicStorage()
if err != nil {
t.Fatal("failed to create basic map storage:", err)
}
SeedEmbeddedHashes(hs)
}
func TestDecodeHashesV3(t *testing.T) {
t.Cleanup(func() { ch.SafeOnly = false })
var all []ch.SavedHashV2
all = append(all, sample1SavedHashes...)
all = append(all, sample2SavedHashes...)
all = append(all, sample3SavedHashes...)
all = append(all, sample4SavedHashes...)
all = append(all, exampleSavedHashes...)
want, err := ch.NewSavedHashesV3(&ch.SavedHashesV2{Hashes: all})
if err != nil {
t.Fatal("failed to build saved hashes:", err)
}
var buf bytes.Buffer
if err := ch.EncodeHashesV3(want, &buf); err != nil {
t.Fatal("failed to encode hashes:", err)
}
for _, safeOnly := range []bool{false, true} {
t.Run(fmt.Sprintf("SafeOnly=%v", safeOnly), func(t *testing.T) {
ch.SafeOnly = safeOnly
got, err := ch.DecodeHashesV3(bytes.NewReader(buf.Bytes()))
if err != nil {
t.Fatal("failed to decode hashes:", err)
}
if diff := cmp.Diff(want, got, cmpOpts...); diff != "" {
t.Errorf("DecodeHashesV3() mismatch (-want +got):\n%s", diff)
}
})
}
}
func BenchmarkLoadEmbeddedHashes(b *testing.B) {
hs, err := storage.NewBasicStorage()
if err != nil {
b.Fatal("failed to create basic map storage:", err)
}
for b.Loop() {
SeedEmbeddedHashes(hs)
}
}
func BenchmarkDecodeHashes(b *testing.B) {
hashes := unzipEmbedded()
hs, err := storage.NewBasicStorage()
if err != nil {
b.Fatal("failed to create basic map storage:", err)
}
for b.Loop() {
decodeHashes(hs, hashes)
}
}
func decodeHashes(hs ch.HashStorage, hashes []byte) {
loadedHashes, err := ch.DecodeHashesV3(bytes.NewReader(hashes))
if err != nil {
panic(err)
}
hs.DecodeHashes(loadedHashes)
}
func unzipEmbedded() []byte {
hashes := ch.Hashes
if gr, err := gzip.NewReader(bytes.NewReader(ch.Hashes)); err == nil {
hashes, err = io.ReadAll(gr)
if err != nil {
log.Println(log.Fatal, "Failed to read embedded hashes:", err)
}
gr.Close()
}
return hashes
}
func SeedEmbeddedHashes(hs ch.HashStorage) {
decodeHashes(hs, unzipEmbedded())
}
func BenchmarkStorageSmall(fuck *testing.B) {
Match := []ch.Hash{
{Hash: 0xfc788d1d89c1c9cf, Kind: goimagehash.AHash},
{Hash: 0x61d91b79330f939a, Kind: goimagehash.DHash},
{Hash: 0xf95ac0669da8b4a9, Kind: goimagehash.PHash},
}
hs, err := storage.NewBasicStorage()
if err != nil {
fuck.Fatal("Failed to create Basic storage:", err)
}
SeedEmbeddedHashes(hs)
runtime.GC()
runtime.GC()
m := runtime.MemStats{}
runtime.ReadMemStats(&m)
fuck.Run("Basic Storage", func(b *testing.B) {
for b.Loop() {
b.ReportMetric(float64(m.HeapAlloc), "bytes")
hs.GetMatches(Match, 8, true)
}
})
hs, err = storage.NewMapStorage()
if err != nil {
fuck.Fatal("Failed to create Map storage:", err)
}
SeedEmbeddedHashes(hs)
runtime.GC()
runtime.GC()
m = runtime.MemStats{}
runtime.ReadMemStats(&m)
fuck.Run("Map Storage", func(b *testing.B) {
for b.Loop() {
b.ReportMetric(float64(m.HeapAlloc), "bytes")
hs.GetMatches(Match, 8, true)
}
})
hs, err = storage.NewVPStorage()
if err != nil {
fuck.Fatal("Failed to create VPS storage:", err)
}
SeedEmbeddedHashes(hs)
runtime.GC()
runtime.GC()
m = runtime.MemStats{}
runtime.ReadMemStats(&m)
fuck.Run("VPS Storage", func(b *testing.B) {
for b.Loop() {
b.ReportMetric(float64(m.HeapAlloc), "bytes")
hs.GetMatches(Match, 8, true)
}
})
}
func BenchmarkStorageLarge(b *testing.B) {
Match := []ch.Hash{
{Hash: 0x003c7e7e7e7e3c18, Kind: goimagehash.AHash},
{Hash: 0x71f0f4d2f0b8f071, Kind: goimagehash.DHash},
{Hash: 0x95274a996be4349e, Kind: goimagehash.PHash},
}
hs, err := storage.NewBasicStorage()
if err != nil {
b.Fatal("Failed to create Basic storage:", err)
}
SeedEmbeddedHashes(hs)
runtime.GC()
runtime.GC()
m := runtime.MemStats{}
runtime.ReadMemStats(&m)
b.Run("Basic Storage", func(b *testing.B) {
for b.Loop() {
b.ReportMetric(float64(m.HeapAlloc), "bytes")
hs.GetMatches(Match, 8, true)
}
})
hs, err = storage.NewMapStorage()
if err != nil {
b.Fatal("Failed to create Map storage:", err)
}
SeedEmbeddedHashes(hs)
runtime.GC()
runtime.GC()
m = runtime.MemStats{}
runtime.ReadMemStats(&m)
b.Run("Map Storage", func(b *testing.B) {
for b.Loop() {
b.ReportMetric(float64(m.HeapAlloc), "bytes")
hs.GetMatches(Match, 8, true)
}
})
hs, err = storage.NewVPStorage()
if err != nil {
b.Fatal("Failed to create VPS storage:", err)
}
SeedEmbeddedHashes(hs)
runtime.GC()
runtime.GC()
m = runtime.MemStats{}
runtime.ReadMemStats(&m)
b.Run("VPS Storage", func(b *testing.B) {
for b.Loop() {
b.ReportMetric(float64(m.HeapAlloc), "bytes")
hs.GetMatches(Match, 8, true)
}
})
}