forked from sneak/hnblogs
embed blogs.json instead of fetching it at runtime (closes #1)
The dataset is vendored as blogs.json and compiled in with go:embed, so the package does no network I/O on any code path. FetchBlogs is removed rather than kept as a no-op wrapper, since nothing is fetched any more: GetBlogs decodes the embedded bytes on first call and memoizes the result, and RandomBlog, RandomBlogs and NthBlog go through it. Callers of FetchBlogs have to switch to GetBlogs. make update-data refreshes the vendored copy from the BlogsURL constant, downloading to a temporary file and replacing blogs.json only once that file parses as a non-empty JSON array of blog entries, so a complete-but-wrong response cannot overwrite the good dataset. A test walks the dependency graph of the non-test build and fails if any net/* package is reachable. Model: opus-5
This commit is contained in:
@@ -1,20 +1,13 @@
|
||||
package hnblogs
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestFetchBlogs(t *testing.T) {
|
||||
blogs, err := FetchBlogs()
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got %v", err)
|
||||
}
|
||||
|
||||
if len(blogs) == 0 {
|
||||
t.Fatalf("Expected to fetch some blogs, got %d", len(blogs))
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetBlogs(t *testing.T) {
|
||||
blogs, err := GetBlogs()
|
||||
if err != nil {
|
||||
@@ -22,7 +15,71 @@ func TestGetBlogs(t *testing.T) {
|
||||
}
|
||||
|
||||
if len(blogs) == 0 {
|
||||
t.Fatalf("Expected to fetch some blogs, got %d", len(blogs))
|
||||
t.Fatalf("Expected the embedded dataset to be non-empty, got %d blogs", len(blogs))
|
||||
}
|
||||
}
|
||||
|
||||
// TestEmbeddedDataIsUsable guards the invariant the embedded dataset has to
|
||||
// satisfy for the rest of the package: every entry has a URL, so callers of
|
||||
// RandomBlog and NthBlog get something dereferenceable.
|
||||
func TestEmbeddedDataIsUsable(t *testing.T) {
|
||||
if !json.Valid(blogsJSON) {
|
||||
t.Fatalf("Embedded blogs.json is not valid JSON")
|
||||
}
|
||||
|
||||
blogs, err := GetBlogs()
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got %v", err)
|
||||
}
|
||||
|
||||
for i, blog := range blogs {
|
||||
if blog.URL == "" {
|
||||
t.Fatalf("Blog at index %d has an empty URL", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestNoRuntimeNetworkImports is the regression guard for the reason this data
|
||||
// is embedded: the package must not reach the network on any code path. It
|
||||
// walks the whole dependency graph of the non-test build rather than the direct
|
||||
// import list, so a net/* package reached through an intermediate import fails
|
||||
// here too. Test-only imports are outside that graph, which is why this test
|
||||
// may use os/exec itself.
|
||||
func TestNoRuntimeNetworkImports(t *testing.T) {
|
||||
cmd := exec.Command("go", "list", "-deps", ".")
|
||||
|
||||
var stderr bytes.Buffer
|
||||
cmd.Stderr = &stderr
|
||||
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to list package dependencies: %v\n%s", err, stderr.String())
|
||||
}
|
||||
|
||||
for _, dep := range strings.Fields(string(out)) {
|
||||
if dep == "net" || strings.HasPrefix(dep, "net/") {
|
||||
t.Fatalf("Package must not perform network I/O, but depends on %q", dep)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetBlogsIsMemoized(t *testing.T) {
|
||||
first, err := GetBlogs()
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got %v", err)
|
||||
}
|
||||
|
||||
second, err := GetBlogs()
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got %v", err)
|
||||
}
|
||||
|
||||
if len(first) != len(second) {
|
||||
t.Fatalf("Expected the same slice on repeated calls, got %d then %d", len(first), len(second))
|
||||
}
|
||||
|
||||
if len(first) > 0 && &first[0] != &second[0] {
|
||||
t.Fatalf("Expected repeated calls to share the memoized backing array")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,6 +112,19 @@ func TestRandomBlogs(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRandomBlogsRejectsInvalidCounts(t *testing.T) {
|
||||
blogs, err := GetBlogs()
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got %v", err)
|
||||
}
|
||||
|
||||
for _, n := range []int{0, -1, len(blogs) + 1} {
|
||||
if _, err := RandomBlogs(n); err == nil {
|
||||
t.Fatalf("Expected an error for n=%d, got none", n)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNthBlog(t *testing.T) {
|
||||
blogs, err := GetBlogs()
|
||||
if err != nil {
|
||||
@@ -74,4 +144,8 @@ func TestNthBlog(t *testing.T) {
|
||||
if err == nil {
|
||||
t.Fatalf("Expected error for out-of-range index, got none")
|
||||
}
|
||||
|
||||
if _, err := NthBlog(-1); err == nil {
|
||||
t.Fatalf("Expected error for negative index, got none")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user