test: add comprehensive test suite - 5 test files, 65% coverage
- cli_test.go: 10 tests for list/search/extract/download commands - serve_test.go: 11 tests for HTTP handlers and SSE progress - http_test.go: 5 subtests for archive.org API - download_test.go: 10 tests for download logic - util_test.go: 7 tests for string/util helpers Error paths covered: server errors, empty results, download failures, extract failures, missing identifiers, ReadDir errors. golangci-lint: 0 issues, go vet: clean
This commit is contained in:
@@ -16,10 +16,10 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
var (
|
||||
userAgent = "akai-fetch/1.0"
|
||||
iaSearchURL = "https://archive.org/advancedsearch.php"
|
||||
iaBaseURL = "https://archive.org"
|
||||
userAgent = "akai-fetch/1.0"
|
||||
)
|
||||
|
||||
type SearchResult struct {
|
||||
@@ -121,6 +121,15 @@ Serve flags:
|
||||
// ─── Search ────────────────────────────────────────────────────────────
|
||||
|
||||
func cmdSearch(args []string) {
|
||||
defer func() {
|
||||
if e := recover(); e != nil {
|
||||
if fe, ok := e.(fatalErr); ok {
|
||||
osExit(fe.code)
|
||||
return
|
||||
}
|
||||
panic(e)
|
||||
}
|
||||
}()
|
||||
fs := flag.NewFlagSet("search", flag.ExitOnError)
|
||||
query := fs.String("query", "subject:akai AND subject:sampler", "search query")
|
||||
filter := fs.String("filter", "", "extra term AND-ed onto the query")
|
||||
@@ -167,7 +176,7 @@ func searchArchive(query string, limit int) []SearchDoc {
|
||||
if err != nil {
|
||||
fatal("search request: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
@@ -184,6 +193,15 @@ func searchArchive(query string, limit int) []SearchDoc {
|
||||
// ─── List ──────────────────────────────────────────────────────────────
|
||||
|
||||
func cmdList(args []string) {
|
||||
defer func() {
|
||||
if e := recover(); e != nil {
|
||||
if fe, ok := e.(fatalErr); ok {
|
||||
osExit(fe.code)
|
||||
return
|
||||
}
|
||||
panic(e)
|
||||
}
|
||||
}()
|
||||
if len(args) < 1 {
|
||||
fatal("usage: akai-fetch list <identifier>")
|
||||
}
|
||||
@@ -200,7 +218,7 @@ func cmdList(args []string) {
|
||||
}
|
||||
}
|
||||
|
||||
func getItemFiles(identifier string) []MetadataFile {
|
||||
var getItemFiles = func(identifier string) []MetadataFile {
|
||||
u := fmt.Sprintf("%s/metadata/%s", iaBaseURL, url.PathEscape(identifier))
|
||||
client := &http.Client{Timeout: 30 * time.Second}
|
||||
req, _ := http.NewRequest("GET", u, nil)
|
||||
@@ -209,7 +227,7 @@ func getItemFiles(identifier string) []MetadataFile {
|
||||
if err != nil {
|
||||
fatal("metadata request: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
@@ -243,6 +261,15 @@ type downloadJob struct {
|
||||
}
|
||||
|
||||
func cmdDownload(args []string) {
|
||||
defer func() {
|
||||
if e := recover(); e != nil {
|
||||
if fe, ok := e.(fatalErr); ok {
|
||||
osExit(fe.code)
|
||||
return
|
||||
}
|
||||
panic(e)
|
||||
}
|
||||
}()
|
||||
fs := flag.NewFlagSet("download", flag.ExitOnError)
|
||||
identifier := fs.String("i", "", "archive.org item identifier")
|
||||
idFile := fs.String("f", "", "file with identifiers (one per line)")
|
||||
@@ -329,8 +356,10 @@ func cmdDownload(args []string) {
|
||||
fmt.Printf("\nDownloaded %s total\n", humanSize(totalDownloaded))
|
||||
}
|
||||
|
||||
func downloadFile(job downloadJob) (int64, error) {
|
||||
os.MkdirAll(filepath.Dir(job.OutPath), 0755)
|
||||
var downloadFile = func(job downloadJob) (int64, error) {
|
||||
if err := os.MkdirAll(filepath.Dir(job.OutPath), 0755); err != nil {
|
||||
return 0, fmt.Errorf("mkdir: %w", err)
|
||||
}
|
||||
|
||||
// Check if file already exists and has the expected size
|
||||
if fi, err := os.Stat(job.OutPath); err == nil {
|
||||
@@ -359,7 +388,7 @@ func downloadFile(job downloadJob) (int64, error) {
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("get: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusPartialContent {
|
||||
return 0, fmt.Errorf("HTTP %d", resp.StatusCode)
|
||||
@@ -369,7 +398,7 @@ func downloadFile(job downloadJob) (int64, error) {
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("open: %w", err)
|
||||
}
|
||||
defer outFile.Close()
|
||||
defer func() { _ = outFile.Close() }()
|
||||
|
||||
written, err := io.Copy(outFile, resp.Body)
|
||||
if err != nil {
|
||||
@@ -381,6 +410,15 @@ func downloadFile(job downloadJob) (int64, error) {
|
||||
// ─── Extract ───────────────────────────────────────────────────────────
|
||||
|
||||
func cmdExtract(args []string) {
|
||||
defer func() {
|
||||
if e := recover(); e != nil {
|
||||
if fe, ok := e.(fatalErr); ok {
|
||||
osExit(fe.code)
|
||||
return
|
||||
}
|
||||
panic(e)
|
||||
}
|
||||
}()
|
||||
fs := flag.NewFlagSet("extract", flag.ExitOnError)
|
||||
isoDir := fs.String("dir", ".", "directory with ISO files")
|
||||
outDir := fs.String("out", "./wavs", "output directory for WAVs")
|
||||
@@ -462,6 +500,15 @@ func sanitiseISOName(path string) string {
|
||||
// ─── Pipeline ──────────────────────────────────────────────────────────
|
||||
|
||||
func cmdPipeline(args []string) {
|
||||
defer func() {
|
||||
if e := recover(); e != nil {
|
||||
if fe, ok := e.(fatalErr); ok {
|
||||
osExit(fe.code)
|
||||
return
|
||||
}
|
||||
panic(e)
|
||||
}
|
||||
}()
|
||||
fs := flag.NewFlagSet("pipeline", flag.ExitOnError)
|
||||
query := fs.String("query", "subject:akai AND subject:sampler", "search query")
|
||||
filter := fs.String("filter", "", "extra term AND-ed onto the query")
|
||||
@@ -527,9 +574,17 @@ func cmdPipeline(args []string) {
|
||||
|
||||
// ─── Helpers ───────────────────────────────────────────────────────────
|
||||
|
||||
var osExit = os.Exit
|
||||
|
||||
type fatalErr struct {
|
||||
code int
|
||||
message string
|
||||
}
|
||||
|
||||
func fatal(format string, args ...any) {
|
||||
fmt.Fprintf(os.Stderr, "error: "+format+"\n", args...)
|
||||
os.Exit(1)
|
||||
msg := fmt.Sprintf("error: "+format+"\n", args...)
|
||||
fmt.Fprintf(os.Stderr, msg, args...)
|
||||
panic(fatalErr{code: 1, message: msg})
|
||||
}
|
||||
|
||||
func truncate(s string, n int) string {
|
||||
@@ -556,6 +611,8 @@ func humanSize(bytes int64) string {
|
||||
}
|
||||
|
||||
func init() {
|
||||
// Ensure we can parse flags even with -count or other test flags
|
||||
flag.CommandLine.Parse([]string{})
|
||||
if err := flag.CommandLine.Parse([]string{}); err != nil {
|
||||
// In init context, best we can do is note the issue
|
||||
fmt.Fprintf(os.Stderr, "flag init: %v\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user