mirror of
https://github.com/hanzoai/forge.git
synced 2026-10-30 15:00:39 +00:00
Extends the Go module cache to cargo and conan, so one registry fronts the ecosystems our builds actually pull from. CARGO. crates.io does not serve the publish wire format the local parser reads, so the cache synthesises it (uint32-framed metadata + .crate) and hands it to the SAME cargo_module.ParsePackage the publish path uses -- a hand-built Package struct would have duplicated the name/semver/URL/dep-rename validation and could drift into accepting what upload refuses. It enforces the published sha256 unconditionally: an upstream that will not say what the bytes hash to does not get cached. Deps are fetched rather than left empty because our sparse index is built from stored metadata, and an entry claiming 'no dependencies' is a WRONG answer, worse than a missing one. Yanked state is carried from upstream, so a lockfile pinned to a yanked crate still resolves. CONAN. A recipe is many files across recipe and package revisions, so the cache mirrors a revision's file set and REFUSES to resolve /latest. Pinning whatever ConanCenter published at the moment of a cold request into a name that promises immutability would be worse than a miss. Upload and cache were refactored to share one literal writer (storeFile), not a same-shaped copy. Known ceilings, stated not hidden: a plain on a cold cache still 404s (it fills for lockfiles and explicit revisions); conan v1 and binary packages are uncached; a cached crate loses a dependency RENAME because the crates.io API omits explicit_name_in_toml. Build, vet and tests green across all three caches. Co-authored-by: Hanzo Dev <[email protected]>
149 lines
5.6 KiB
Go
149 lines
5.6 KiB
Go
// Copyright 2026 Hanzo AI, Inc. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package conan
|
|
|
|
import (
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
conan_module "github.com/hanzoai/git/modules/packages/conan"
|
|
"github.com/hanzoai/git/modules/setting"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestUpstreamEnabled(t *testing.T) {
|
|
defer func(v string) { setting.Packages.ConanUpstream = v }(setting.Packages.ConanUpstream)
|
|
|
|
// Empty means publish-only -- the behaviour every deployment has until
|
|
// someone opts in. Whitespace must not accidentally enable it.
|
|
setting.Packages.ConanUpstream = ""
|
|
assert.False(t, upstreamEnabled())
|
|
setting.Packages.ConanUpstream = " "
|
|
assert.False(t, upstreamEnabled())
|
|
|
|
setting.Packages.ConanUpstream = "https://center.conan.io"
|
|
assert.True(t, upstreamEnabled())
|
|
}
|
|
|
|
func TestIsPrivateRecipe(t *testing.T) {
|
|
defer func(v string) { setting.Packages.ConanPrivate = v }(setting.Packages.ConanPrivate)
|
|
setting.Packages.ConanPrivate = "hanzo-,lux-,luxcpp,zoo-"
|
|
|
|
// Private names must never reach a public remote -- the name alone leaks an
|
|
// internal library that may not be public yet.
|
|
assert.True(t, isPrivateRecipe("lux-crypto"))
|
|
assert.True(t, isPrivateRecipe("luxcpp"))
|
|
assert.True(t, isPrivateRecipe("hanzo-runtime"))
|
|
|
|
// Public recipes still cache.
|
|
assert.False(t, isPrivateRecipe("zlib"))
|
|
assert.False(t, isPrivateRecipe("boost"))
|
|
// Near-misses must NOT be treated as private (no accidental over-blocking).
|
|
assert.False(t, isPrivateRecipe("luxon"))
|
|
assert.False(t, isPrivateRecipe("hanzoai"))
|
|
assert.False(t, isPrivateRecipe("zoology"))
|
|
|
|
// Empty config blocks nothing.
|
|
setting.Packages.ConanPrivate = ""
|
|
assert.False(t, isPrivateRecipe("lux-crypto"))
|
|
}
|
|
|
|
func TestHasConcreteRevision(t *testing.T) {
|
|
newRef := func(t *testing.T, revision string) *conan_module.RecipeReference {
|
|
t.Helper()
|
|
rref, err := conan_module.NewRecipeReference("zlib", "1.3.1", "", "", revision)
|
|
require.NoError(t, err)
|
|
return rref
|
|
}
|
|
|
|
// Only a real revision names one immutable thing. No revision at all, or the
|
|
// "0" sentinel every v1 URL arrives as, is a question -- not something that
|
|
// can be fetched without guessing.
|
|
assert.False(t, hasConcreteRevision(newRef(t, "")))
|
|
assert.False(t, hasConcreteRevision(newRef(t, conan_module.DefaultRevision)))
|
|
assert.True(t, hasConcreteRevision(newRef(t, "6a7e1a92e3c5e1d5f3b8b8f6e9d0c1a2")))
|
|
}
|
|
|
|
func TestUpstreamRecipePath(t *testing.T) {
|
|
rref, err := conan_module.NewRecipeReference("zlib", "1.3.1", "", "", "abc123")
|
|
require.NoError(t, err)
|
|
elem, ok := upstreamRecipePath(rref)
|
|
require.True(t, ok)
|
|
// A recipe with no user/channel is "_/_" upstream, not an empty segment.
|
|
assert.Equal(t, "v2/conans/zlib/1.3.1/_/_/revisions/abc123", elem)
|
|
|
|
rref, err = conan_module.NewRecipeReference("zlib", "1.3.1", "lux", "stable", "abc123")
|
|
require.NoError(t, err)
|
|
elem, ok = upstreamRecipePath(rref)
|
|
require.True(t, ok)
|
|
assert.Equal(t, "v2/conans/zlib/1.3.1/lux/stable/revisions/abc123", elem)
|
|
|
|
// NewRecipeReference only requires the version to be non-empty, so a dot
|
|
// segment gets this far. It must never become part of an upstream URL.
|
|
rref, err = conan_module.NewRecipeReference("zlib", "..", "", "", "abc123")
|
|
require.NoError(t, err)
|
|
_, ok = upstreamRecipePath(rref)
|
|
assert.False(t, ok)
|
|
|
|
rref, err = conan_module.NewRecipeReference("zlib", "1.3.1/../../etc", "", "", "abc123")
|
|
require.NoError(t, err)
|
|
_, ok = upstreamRecipePath(rref)
|
|
assert.False(t, ok)
|
|
}
|
|
|
|
func TestUpstreamFetch(t *testing.T) {
|
|
defer func(v string) { setting.Packages.ConanUpstream = v }(setting.Packages.ConanUpstream)
|
|
|
|
var gotPath string
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
gotPath = r.URL.Path
|
|
if r.URL.Path == "/v2/conans/zlib/1.3.1/_/_/revisions/abc123/files/conanfile.py" {
|
|
w.Write([]byte("from conan import ConanFile"))
|
|
return
|
|
}
|
|
w.WriteHeader(http.StatusNotFound)
|
|
}))
|
|
defer srv.Close()
|
|
|
|
// A trailing slash on the configured base must not produce a doubled slash.
|
|
setting.Packages.ConanUpstream = srv.URL + "/"
|
|
body, _, err := upstreamFetch("v2/conans/zlib/1.3.1/_/_/revisions/abc123/files/conanfile.py")
|
|
require.NoError(t, err)
|
|
defer body.Close()
|
|
assert.Equal(t, "/v2/conans/zlib/1.3.1/_/_/revisions/abc123/files/conanfile.py", gotPath)
|
|
|
|
// A non-200 is an error, never an empty body silently cached as a recipe.
|
|
setting.Packages.ConanUpstream = srv.URL
|
|
_, _, err = upstreamFetch("v2/conans/zlib/9.9.9/_/_/revisions/abc123/files/conanfile.py")
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestUpstreamRecipeFileList(t *testing.T) {
|
|
defer func(v string) { setting.Packages.ConanUpstream = v }(setting.Packages.ConanUpstream)
|
|
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path != "/v2/conans/zlib/1.3.1/_/_/revisions/abc123/files" {
|
|
w.WriteHeader(http.StatusNotFound)
|
|
return
|
|
}
|
|
w.Write([]byte(`{"files": {"conanfile.py": {}, "conanmanifest.txt": {}, "conan_export.tgz": {}, "evil.sh": {}}}`))
|
|
}))
|
|
defer srv.Close()
|
|
setting.Packages.ConanUpstream = srv.URL
|
|
|
|
names, err := upstreamRecipeFileList("v2/conans/zlib/1.3.1/_/_/revisions/abc123")
|
|
require.NoError(t, err)
|
|
// Sorted, and filtered to what the upload path accepts -- the cache can only
|
|
// ever hold files this registry would have taken from a publish.
|
|
assert.Equal(t, []string{"conan_export.tgz", "conanfile.py", "conanmanifest.txt"}, names)
|
|
|
|
// An upstream that does not have the revision is an error, so the caller
|
|
// falls back to the ordinary not-found.
|
|
_, err = upstreamRecipeFileList("v2/conans/zlib/9.9.9/_/_/revisions/abc123")
|
|
require.Error(t, err)
|
|
}
|