mirror of
https://github.com/ipfs/kubo.git
synced 2026-02-21 10:27:46 +08:00
Merge branch 'master' into test-cleanup
This commit is contained in:
commit
29dc1e59ca
124
.github/workflows/gotest.yml
vendored
124
.github/workflows/gotest.yml
vendored
@ -14,11 +14,13 @@ concurrency:
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
go-test:
|
||||
# Unit tests with coverage collection (uploaded to Codecov)
|
||||
unit-tests:
|
||||
if: github.repository == 'ipfs/kubo' || github.event_name == 'workflow_dispatch'
|
||||
runs-on: ${{ fromJSON(github.repository == 'ipfs/kubo' && '["self-hosted", "linux", "x64", "2xlarge"]' || '"ubuntu-latest"') }}
|
||||
timeout-minutes: 20
|
||||
timeout-minutes: 15
|
||||
env:
|
||||
GOTRACEBACK: single # reduce noise on test timeout panics
|
||||
TEST_DOCKER: 0
|
||||
TEST_FUSE: 0
|
||||
TEST_VERBOSE: 1
|
||||
@ -36,12 +38,9 @@ jobs:
|
||||
go-version-file: 'go.mod'
|
||||
- name: Install missing tools
|
||||
run: sudo apt update && sudo apt install -y zsh
|
||||
- name: 👉️ If this step failed, go to «Summary» (top left) → inspect the «Failures/Errors» table
|
||||
env:
|
||||
# increasing parallelism beyond 2 doesn't speed up the tests much
|
||||
PARALLEL: 2
|
||||
- name: Run unit tests
|
||||
run: |
|
||||
make -j "$PARALLEL" test/unit/gotest.junit.xml &&
|
||||
make test_unit &&
|
||||
[[ ! $(jq -s -c 'map(select(.Action == "fail")) | .[]' test/unit/gotest.json) ]]
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
|
||||
@ -49,28 +48,8 @@ jobs:
|
||||
with:
|
||||
name: unittests
|
||||
files: coverage/unit_tests.coverprofile
|
||||
- name: Test kubo-as-a-library example
|
||||
run: |
|
||||
# we want to first test with the kubo version in the go.mod file
|
||||
go test -v ./...
|
||||
|
||||
# we also want to test the examples against the current version of kubo
|
||||
# however, that version might be in a fork so we need to replace the dependency
|
||||
|
||||
# backup the go.mod and go.sum files to restore them after we run the tests
|
||||
cp go.mod go.mod.bak
|
||||
cp go.sum go.sum.bak
|
||||
|
||||
# make sure the examples run against the current version of kubo
|
||||
go mod edit -replace github.com/ipfs/kubo=./../../..
|
||||
go mod tidy
|
||||
|
||||
go test -v ./...
|
||||
|
||||
# restore the go.mod and go.sum files to their original state
|
||||
mv go.mod.bak go.mod
|
||||
mv go.sum.bak go.sum
|
||||
working-directory: docs/examples/kubo-as-a-library
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
fail_ci_if_error: false
|
||||
- name: Create a proper JUnit XML report
|
||||
uses: ipdxco/gotest-json-to-junit-xml@v1
|
||||
with:
|
||||
@ -80,7 +59,7 @@ jobs:
|
||||
- name: Archive the JUnit XML report
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: unit
|
||||
name: unit-tests-junit
|
||||
path: test/unit/gotest.junit.xml
|
||||
if: failure() || success()
|
||||
- name: Create a HTML report
|
||||
@ -93,7 +72,7 @@ jobs:
|
||||
- name: Archive the HTML report
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: html
|
||||
name: unit-tests-html
|
||||
path: test/unit/gotest.html
|
||||
if: failure() || success()
|
||||
- name: Create a Markdown report
|
||||
@ -106,3 +85,86 @@ jobs:
|
||||
- name: Set the summary
|
||||
run: cat test/unit/gotest.md >> $GITHUB_STEP_SUMMARY
|
||||
if: failure() || success()
|
||||
|
||||
# End-to-end integration/regression tests from test/cli
|
||||
# (Go-based replacement for legacy test/sharness shell scripts)
|
||||
cli-tests:
|
||||
if: github.repository == 'ipfs/kubo' || github.event_name == 'workflow_dispatch'
|
||||
runs-on: ${{ fromJSON(github.repository == 'ipfs/kubo' && '["self-hosted", "linux", "x64", "2xlarge"]' || '"ubuntu-latest"') }}
|
||||
timeout-minutes: 15
|
||||
env:
|
||||
GOTRACEBACK: single # reduce noise on test timeout panics
|
||||
TEST_VERBOSE: 1
|
||||
GIT_PAGER: cat
|
||||
IPFS_CHECK_RCMGR_DEFAULTS: 1
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
steps:
|
||||
- name: Check out Kubo
|
||||
uses: actions/checkout@v6
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: 'go.mod'
|
||||
- name: Install missing tools
|
||||
run: sudo apt update && sudo apt install -y zsh
|
||||
- name: Run CLI tests
|
||||
env:
|
||||
IPFS_PATH: ${{ runner.temp }}/ipfs-test
|
||||
run: make test_cli
|
||||
- name: Create JUnit XML report
|
||||
uses: ipdxco/gotest-json-to-junit-xml@v1
|
||||
with:
|
||||
input: test/cli/cli-tests.json
|
||||
output: test/cli/cli-tests.junit.xml
|
||||
if: failure() || success()
|
||||
- name: Archive JUnit XML report
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: cli-tests-junit
|
||||
path: test/cli/cli-tests.junit.xml
|
||||
if: failure() || success()
|
||||
- name: Create HTML report
|
||||
uses: ipdxco/junit-xml-to-html@v1
|
||||
with:
|
||||
mode: no-frames
|
||||
input: test/cli/cli-tests.junit.xml
|
||||
output: test/cli/cli-tests.html
|
||||
if: failure() || success()
|
||||
- name: Archive HTML report
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: cli-tests-html
|
||||
path: test/cli/cli-tests.html
|
||||
if: failure() || success()
|
||||
- name: Create Markdown report
|
||||
uses: ipdxco/junit-xml-to-html@v1
|
||||
with:
|
||||
mode: summary
|
||||
input: test/cli/cli-tests.junit.xml
|
||||
output: test/cli/cli-tests.md
|
||||
if: failure() || success()
|
||||
- name: Set summary
|
||||
run: cat test/cli/cli-tests.md >> $GITHUB_STEP_SUMMARY
|
||||
if: failure() || success()
|
||||
|
||||
# Example tests (kubo-as-a-library)
|
||||
example-tests:
|
||||
if: github.repository == 'ipfs/kubo' || github.event_name == 'workflow_dispatch'
|
||||
runs-on: ${{ fromJSON(github.repository == 'ipfs/kubo' && '["self-hosted", "linux", "x64", "2xlarge"]' || '"ubuntu-latest"') }}
|
||||
timeout-minutes: 5
|
||||
env:
|
||||
GOTRACEBACK: single
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
steps:
|
||||
- name: Check out Kubo
|
||||
uses: actions/checkout@v6
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: 'go.mod'
|
||||
- name: Run example tests
|
||||
run: make test_examples
|
||||
|
||||
2
.github/workflows/sharness.yml
vendored
2
.github/workflows/sharness.yml
vendored
@ -60,6 +60,8 @@ jobs:
|
||||
with:
|
||||
name: sharness
|
||||
files: kubo/coverage/sharness_tests.coverprofile
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
fail_ci_if_error: false
|
||||
- name: Aggregate results
|
||||
run: find kubo/test/sharness/test-results -name 't*-*.sh.*.counts' | kubo/test/sharness/lib/sharness/aggregate-results.sh > kubo/test/sharness/test-results/summary.txt
|
||||
- name: 👉️ If this step failed, go to «Summary» (top left) → «HTML Report» → inspect the «Failures» column
|
||||
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@ -28,6 +28,11 @@ go-ipfs-source.tar.gz
|
||||
docs/examples/go-ipfs-as-a-library/example-folder/Qm*
|
||||
/test/sharness/t0054-dag-car-import-export-data/*.car
|
||||
|
||||
# test artifacts from make test_unit / test_cli
|
||||
/test/unit/gotest.json
|
||||
/test/unit/gotest.junit.xml
|
||||
/test/cli/cli-tests.json
|
||||
|
||||
# ignore build output from snapcraft
|
||||
/ipfs_*.snap
|
||||
/parts
|
||||
|
||||
15
Rules.mk
15
Rules.mk
@ -134,15 +134,14 @@ help:
|
||||
@echo ''
|
||||
@echo 'TESTING TARGETS:'
|
||||
@echo ''
|
||||
@echo ' test - Run all tests'
|
||||
@echo ' test_short - Run short go tests and short sharness tests'
|
||||
@echo ' test_go_short - Run short go tests'
|
||||
@echo ' test_go_test - Run all go tests'
|
||||
@echo ' test - Run all tests (test_go_fmt, test_unit, test_cli, test_sharness)'
|
||||
@echo ' test_short - Run fast tests (test_go_fmt, test_unit)'
|
||||
@echo ' test_unit - Run unit tests with coverage (excludes test/cli)'
|
||||
@echo ' test_cli - Run CLI integration tests (requires built binary)'
|
||||
@echo ' test_go_fmt - Check Go source formatting'
|
||||
@echo ' test_go_build - Build kubo for all platforms from .github/build-platforms.yml'
|
||||
@echo ' test_go_expensive - Run all go tests and build all platforms'
|
||||
@echo ' test_go_race - Run go tests with the race detector enabled'
|
||||
@echo ' test_go_lint - Run the `golangci-lint` vetting tool'
|
||||
@echo ' test_go_lint - Run golangci-lint'
|
||||
@echo ' test_sharness - Run sharness tests'
|
||||
@echo ' coverage - Collects coverage info from unit tests and sharness'
|
||||
@echo ' coverage - Collect coverage info from unit tests and sharness'
|
||||
@echo
|
||||
.PHONY: help
|
||||
|
||||
@ -30,6 +30,7 @@ import (
|
||||
const testPeerID = "QmTFauExutTsy4XP6JbMFcw2Wa9645HJt2bTqL6qYDCKfe"
|
||||
|
||||
func TestAddMultipleGCLive(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
r := &repo.Mock{
|
||||
C: config.Config{
|
||||
Identity: config.Identity{
|
||||
@ -38,13 +39,13 @@ func TestAddMultipleGCLive(t *testing.T) {
|
||||
},
|
||||
D: syncds.MutexWrap(datastore.NewMapDatastore()),
|
||||
}
|
||||
node, err := core.NewNode(context.Background(), &core.BuildCfg{Repo: r})
|
||||
node, err := core.NewNode(ctx, &core.BuildCfg{Repo: r})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
out := make(chan interface{}, 10)
|
||||
adder, err := NewAdder(context.Background(), node.Pinning, node.Blockstore, node.DAG)
|
||||
adder, err := NewAdder(ctx, node.Pinning, node.Blockstore, node.DAG)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@ -67,7 +68,7 @@ func TestAddMultipleGCLive(t *testing.T) {
|
||||
|
||||
go func() {
|
||||
defer close(out)
|
||||
_, _ = adder.AddAllAndPin(context.Background(), slf)
|
||||
_, _ = adder.AddAllAndPin(ctx, slf)
|
||||
// Ignore errors for clarity - the real bug would be gc'ing files while adding them, not this resultant error
|
||||
}()
|
||||
|
||||
@ -80,9 +81,12 @@ func TestAddMultipleGCLive(t *testing.T) {
|
||||
gc1started := make(chan struct{})
|
||||
go func() {
|
||||
defer close(gc1started)
|
||||
gc1out = gc.GC(context.Background(), node.Blockstore, node.Repo.Datastore(), node.Pinning, nil)
|
||||
gc1out = gc.GC(ctx, node.Blockstore, node.Repo.Datastore(), node.Pinning, nil)
|
||||
}()
|
||||
|
||||
// Give GC goroutine time to reach GCLock (will block there waiting for adder)
|
||||
time.Sleep(time.Millisecond * 100)
|
||||
|
||||
// GC shouldn't get the lock until after the file is completely added
|
||||
select {
|
||||
case <-gc1started:
|
||||
@ -119,9 +123,12 @@ func TestAddMultipleGCLive(t *testing.T) {
|
||||
gc2started := make(chan struct{})
|
||||
go func() {
|
||||
defer close(gc2started)
|
||||
gc2out = gc.GC(context.Background(), node.Blockstore, node.Repo.Datastore(), node.Pinning, nil)
|
||||
gc2out = gc.GC(ctx, node.Blockstore, node.Repo.Datastore(), node.Pinning, nil)
|
||||
}()
|
||||
|
||||
// Give GC goroutine time to reach GCLock
|
||||
time.Sleep(time.Millisecond * 100)
|
||||
|
||||
select {
|
||||
case <-gc2started:
|
||||
t.Fatal("gc shouldn't have started yet")
|
||||
@ -155,6 +162,7 @@ func TestAddMultipleGCLive(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestAddGCLive(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
r := &repo.Mock{
|
||||
C: config.Config{
|
||||
Identity: config.Identity{
|
||||
@ -163,13 +171,13 @@ func TestAddGCLive(t *testing.T) {
|
||||
},
|
||||
D: syncds.MutexWrap(datastore.NewMapDatastore()),
|
||||
}
|
||||
node, err := core.NewNode(context.Background(), &core.BuildCfg{Repo: r})
|
||||
node, err := core.NewNode(ctx, &core.BuildCfg{Repo: r})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
out := make(chan interface{})
|
||||
adder, err := NewAdder(context.Background(), node.Pinning, node.Blockstore, node.DAG)
|
||||
adder, err := NewAdder(ctx, node.Pinning, node.Blockstore, node.DAG)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@ -193,7 +201,7 @@ func TestAddGCLive(t *testing.T) {
|
||||
go func() {
|
||||
defer close(addDone)
|
||||
defer close(out)
|
||||
_, err := adder.AddAllAndPin(context.Background(), slf)
|
||||
_, err := adder.AddAllAndPin(ctx, slf)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
@ -211,7 +219,7 @@ func TestAddGCLive(t *testing.T) {
|
||||
gcstarted := make(chan struct{})
|
||||
go func() {
|
||||
defer close(gcstarted)
|
||||
gcout = gc.GC(context.Background(), node.Blockstore, node.Repo.Datastore(), node.Pinning, nil)
|
||||
gcout = gc.GC(ctx, node.Blockstore, node.Repo.Datastore(), node.Pinning, nil)
|
||||
}()
|
||||
|
||||
// gc shouldn't start until we let the add finish its current file.
|
||||
@ -255,9 +263,6 @@ func TestAddGCLive(t *testing.T) {
|
||||
last = c
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
|
||||
defer cancel()
|
||||
|
||||
set := cid.NewSet()
|
||||
err = dag.Walk(ctx, dag.GetLinksWithDAG(node.DAG), last, set.Visit)
|
||||
if err != nil {
|
||||
|
||||
@ -3,33 +3,14 @@ include mk/header.mk
|
||||
GOCC ?= go
|
||||
|
||||
$(d)/coverage_deps: $$(DEPS_GO) cmd/ipfs/ipfs
|
||||
rm -rf $(@D)/unitcover && mkdir $(@D)/unitcover
|
||||
rm -rf $(@D)/sharnesscover && mkdir $(@D)/sharnesscover
|
||||
|
||||
ifneq ($(IPFS_SKIP_COVER_BINS),1)
|
||||
$(d)/coverage_deps: test/bin/gocovmerge
|
||||
endif
|
||||
|
||||
.PHONY: $(d)/coverage_deps
|
||||
|
||||
# unit tests coverage
|
||||
UTESTS_$(d) := $(shell $(GOCC) list -f '{{if (or (len .TestGoFiles) (len .XTestGoFiles))}}{{.ImportPath}}{{end}}' $(go-flags-with-tags) ./... | grep -v go-ipfs/vendor | grep -v go-ipfs/Godeps)
|
||||
# unit tests coverage is now produced by test_unit target in mk/golang.mk
|
||||
# (outputs coverage/unit_tests.coverprofile and test/unit/gotest.json)
|
||||
|
||||
UCOVER_$(d) := $(addsuffix .coverprofile,$(addprefix $(d)/unitcover/, $(subst /,_,$(UTESTS_$(d)))))
|
||||
|
||||
$(UCOVER_$(d)): $(d)/coverage_deps ALWAYS
|
||||
$(eval TMP_PKG := $(subst _,/,$(basename $(@F))))
|
||||
$(eval TMP_DEPS := $(shell $(GOCC) list -f '{{range .Deps}}{{.}} {{end}}' $(go-flags-with-tags) $(TMP_PKG) | sed 's/ /\n/g' | grep ipfs/go-ipfs) $(TMP_PKG))
|
||||
$(eval TMP_DEPS_LIST := $(call join-with,$(comma),$(TMP_DEPS)))
|
||||
$(GOCC) test $(go-flags-with-tags) $(GOTFLAGS) -v -covermode=atomic -json -coverpkg=$(TMP_DEPS_LIST) -coverprofile=$@ $(TMP_PKG) | tee -a test/unit/gotest.json
|
||||
|
||||
|
||||
$(d)/unit_tests.coverprofile: $(UCOVER_$(d))
|
||||
gocovmerge $^ > $@
|
||||
|
||||
TGTS_$(d) := $(d)/unit_tests.coverprofile
|
||||
|
||||
.PHONY: $(d)/unit_tests.coverprofile
|
||||
TGTS_$(d) :=
|
||||
|
||||
# sharness tests coverage
|
||||
$(d)/ipfs: GOTAGS += testrunmain
|
||||
@ -46,7 +27,7 @@ endif
|
||||
export IPFS_COVER_DIR:= $(realpath $(d))/sharnesscover/
|
||||
|
||||
$(d)/sharness_tests.coverprofile: export TEST_PLUGIN=0
|
||||
$(d)/sharness_tests.coverprofile: $(d)/ipfs cmd/ipfs/ipfs-test-cover $(d)/coverage_deps test_sharness
|
||||
$(d)/sharness_tests.coverprofile: $(d)/ipfs cmd/ipfs/ipfs-test-cover $(d)/coverage_deps test/bin/gocovmerge test_sharness
|
||||
(cd $(@D)/sharnesscover && find . -type f | gocovmerge -list -) > $@
|
||||
|
||||
|
||||
|
||||
@ -47,7 +47,7 @@ func setupPlugins(externalPluginsPath string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func createTempRepo(swarmPort int) (string, error) {
|
||||
func createTempRepo() (string, error) {
|
||||
repoPath, err := os.MkdirTemp("", "ipfs-shell")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get temp dir: %s", err)
|
||||
@ -59,15 +59,28 @@ func createTempRepo(swarmPort int) (string, error) {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Configure custom ports to avoid conflicts with other IPFS instances.
|
||||
// This demonstrates how to customize the node's network addresses.
|
||||
// Use TCP-only on loopback with random port for reliable local testing.
|
||||
// This matches what kubo's test harness uses (test/cli/transports_test.go).
|
||||
// QUIC/UDP transports are avoided because they may be throttled on CI.
|
||||
cfg.Addresses.Swarm = []string{
|
||||
fmt.Sprintf("/ip4/0.0.0.0/tcp/%d", swarmPort),
|
||||
fmt.Sprintf("/ip4/0.0.0.0/udp/%d/quic-v1", swarmPort),
|
||||
fmt.Sprintf("/ip4/0.0.0.0/udp/%d/quic-v1/webtransport", swarmPort),
|
||||
fmt.Sprintf("/ip4/0.0.0.0/udp/%d/webrtc-direct", swarmPort),
|
||||
"/ip4/127.0.0.1/tcp/0",
|
||||
}
|
||||
|
||||
// Explicitly disable non-TCP transports for reliability.
|
||||
cfg.Swarm.Transports.Network.QUIC = config.False
|
||||
cfg.Swarm.Transports.Network.Relay = config.False
|
||||
cfg.Swarm.Transports.Network.WebTransport = config.False
|
||||
cfg.Swarm.Transports.Network.WebRTCDirect = config.False
|
||||
cfg.Swarm.Transports.Network.Websocket = config.False
|
||||
cfg.AutoTLS.Enabled = config.False
|
||||
|
||||
// Disable routing - we don't need DHT for direct peer connections.
|
||||
// Bitswap works with directly connected peers without needing DHT lookups.
|
||||
cfg.Routing.Type = config.NewOptionalString("none")
|
||||
|
||||
// Disable bootstrap for this example - we manually connect only the peers we need.
|
||||
cfg.Bootstrap = []string{}
|
||||
|
||||
// When creating the repository, you can define custom settings on the repository, such as enabling experimental
|
||||
// features (See experimental-features.md) or customizing the gateway endpoint.
|
||||
// To do such things, you should modify the variable `cfg`. For example:
|
||||
@ -106,10 +119,14 @@ func createNode(ctx context.Context, repoPath string) (*core.IpfsNode, error) {
|
||||
// Construct the node
|
||||
|
||||
nodeOptions := &core.BuildCfg{
|
||||
Online: true,
|
||||
Routing: libp2p.DHTOption, // This option sets the node to be a full DHT node (both fetching and storing DHT Records)
|
||||
// Routing: libp2p.DHTClientOption, // This option sets the node to be a client DHT node (only fetching records)
|
||||
Repo: repo,
|
||||
Online: true,
|
||||
// For this example, we use NilRouterOption (no routing) since we connect peers directly.
|
||||
// Bitswap works with directly connected peers without needing DHT lookups.
|
||||
// In production, you would typically use:
|
||||
// Routing: libp2p.DHTOption, // Full DHT node (stores and fetches records)
|
||||
// Routing: libp2p.DHTClientOption, // DHT client (only fetches records)
|
||||
Routing: libp2p.NilRouterOption,
|
||||
Repo: repo,
|
||||
}
|
||||
|
||||
return core.NewNode(ctx, nodeOptions)
|
||||
@ -118,8 +135,7 @@ func createNode(ctx context.Context, repoPath string) (*core.IpfsNode, error) {
|
||||
var loadPluginsOnce sync.Once
|
||||
|
||||
// Spawns a node to be used just for this run (i.e. creates a tmp repo).
|
||||
// The swarmPort parameter specifies the port for libp2p swarm listeners.
|
||||
func spawnEphemeral(ctx context.Context, swarmPort int) (icore.CoreAPI, *core.IpfsNode, error) {
|
||||
func spawnEphemeral(ctx context.Context) (icore.CoreAPI, *core.IpfsNode, error) {
|
||||
var onceErr error
|
||||
loadPluginsOnce.Do(func() {
|
||||
onceErr = setupPlugins("")
|
||||
@ -129,7 +145,7 @@ func spawnEphemeral(ctx context.Context, swarmPort int) (icore.CoreAPI, *core.Ip
|
||||
}
|
||||
|
||||
// Create a Temporary Repo
|
||||
repoPath, err := createTempRepo(swarmPort)
|
||||
repoPath, err := createTempRepo()
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to create temp repo: %s", err)
|
||||
}
|
||||
@ -207,8 +223,7 @@ func main() {
|
||||
defer cancel()
|
||||
|
||||
// Spawn a local peer using a temporary path, for testing purposes
|
||||
// Using port 4010 to avoid conflict with default IPFS port 4001
|
||||
ipfsA, nodeA, err := spawnEphemeral(ctx, 4010)
|
||||
ipfsA, nodeA, err := spawnEphemeral(ctx)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("failed to spawn peer node: %s", err))
|
||||
}
|
||||
@ -222,9 +237,8 @@ func main() {
|
||||
fmt.Printf("Added file to peer with CID %s\n", peerCidFile.String())
|
||||
|
||||
// Spawn a node using a temporary path, creating a temporary repo for the run
|
||||
// Using port 4011 (different from nodeA's port 4010)
|
||||
fmt.Println("Spawning Kubo node on a temporary repo")
|
||||
ipfsB, _, err := spawnEphemeral(ctx, 4011)
|
||||
ipfsB, _, err := spawnEphemeral(ctx)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("failed to spawn ephemeral node: %s", err))
|
||||
}
|
||||
@ -297,11 +311,12 @@ func main() {
|
||||
|
||||
fmt.Printf("Got directory back from IPFS (IPFS path: %s) and wrote it to %s\n", cidDirectory.String(), outputPathDirectory)
|
||||
|
||||
/// --- Part IV: Getting a file from the IPFS Network
|
||||
/// --- Part IV: Getting a file from another IPFS node
|
||||
|
||||
fmt.Println("\n-- Going to connect to a few nodes in the Network as bootstrappers --")
|
||||
fmt.Println("\n-- Connecting to nodeA and fetching content via bitswap --")
|
||||
|
||||
// Get nodeA's address so we can fetch the file we added to it
|
||||
// Get nodeA's actual listening address dynamically.
|
||||
// We configured TCP-only on 127.0.0.1 with random port, so this will be a TCP address.
|
||||
peerAddrs, err := ipfsA.Swarm().LocalAddrs(ctx)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("could not get peer addresses: %s", err))
|
||||
@ -309,26 +324,18 @@ func main() {
|
||||
peerMa := peerAddrs[0].String() + "/p2p/" + nodeA.Identity.String()
|
||||
|
||||
bootstrapNodes := []string{
|
||||
// In production, use autoconf.FallbackBootstrapPeers from boxo/autoconf
|
||||
// which includes well-known IPFS bootstrap peers like:
|
||||
// In production, use real bootstrap peers like:
|
||||
// "/dnsaddr/bootstrap.libp2p.io/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN",
|
||||
// "/dnsaddr/bootstrap.libp2p.io/p2p/QmQCU2EcMqAqQPR2i9bChDtGNJchTbq5TbXJJ16u19uLTa",
|
||||
// "/ip4/104.131.131.82/tcp/4001/p2p/QmaCpDMGvV2BGHeYERUEnRQAwe3N8SzbUtfsmvsqQLuvuJ",
|
||||
|
||||
// You can add custom peers here. For example, another IPFS node:
|
||||
// "/ip4/192.0.2.1/tcp/4001/p2p/QmYourPeerID...",
|
||||
// "/ip4/192.0.2.1/udp/4001/quic-v1/p2p/QmYourPeerID...",
|
||||
|
||||
// nodeA's address (the peer we created above that has our test file)
|
||||
// For this example, we only connect to nodeA which has our test content.
|
||||
peerMa,
|
||||
}
|
||||
|
||||
fmt.Println("Connecting to peers...")
|
||||
fmt.Println("Connecting to peer...")
|
||||
err = connectToPeers(ctx, ipfsB, bootstrapNodes)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("failed to connect to peers: %s", err))
|
||||
}
|
||||
fmt.Println("Connected to peers")
|
||||
fmt.Println("Connected to peer")
|
||||
|
||||
exampleCIDStr := peerCidFile.RootCid().String()
|
||||
|
||||
|
||||
@ -1,21 +1,39 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestExample(t *testing.T) {
|
||||
out, err := exec.Command("go", "run", "main.go").Output()
|
||||
t.Log("Starting go run main.go...")
|
||||
start := time.Now()
|
||||
|
||||
cmd := exec.Command("go", "run", "main.go")
|
||||
cmd.Env = append(os.Environ(), "GOLOG_LOG_LEVEL=error") // reduce libp2p noise
|
||||
|
||||
// Stream output to both test log and capture buffer for verification
|
||||
// This ensures we see progress even if the process is killed
|
||||
var buf bytes.Buffer
|
||||
cmd.Stdout = io.MultiWriter(os.Stdout, &buf)
|
||||
cmd.Stderr = io.MultiWriter(os.Stderr, &buf)
|
||||
|
||||
err := cmd.Run()
|
||||
|
||||
elapsed := time.Since(start)
|
||||
t.Logf("Command completed in %v", elapsed)
|
||||
|
||||
out := buf.String()
|
||||
if err != nil {
|
||||
var stderr string
|
||||
if xe, ok := err.(*exec.ExitError); ok {
|
||||
stderr = string(xe.Stderr)
|
||||
}
|
||||
t.Fatalf("running example (%v): %s\n%s", err, string(out), stderr)
|
||||
t.Fatalf("running example (%v):\n%s", err, out)
|
||||
}
|
||||
if !strings.Contains(string(out), "All done!") {
|
||||
t.Errorf("example did not run successfully")
|
||||
|
||||
if !strings.Contains(out, "All done!") {
|
||||
t.Errorf("example did not complete successfully, output:\n%s", out)
|
||||
}
|
||||
}
|
||||
|
||||
63
mk/golang.mk
63
mk/golang.mk
@ -41,40 +41,57 @@ define go-build
|
||||
$(GOCC) build $(go-flags-with-tags) -o "$@" "$(1)"
|
||||
endef
|
||||
|
||||
test_go_test: $$(DEPS_GO)
|
||||
$(GOCC) test $(go-flags-with-tags) $(GOTFLAGS) ./...
|
||||
.PHONY: test_go_test
|
||||
# Only disable colors when running in CI (non-interactive terminal)
|
||||
GOTESTSUM_NOCOLOR := $(if $(CI),--no-color,)
|
||||
|
||||
# Build all platforms from .github/build-platforms.yml
|
||||
# Packages excluded from coverage (test code and examples are not production code)
|
||||
COVERPKG_EXCLUDE := /(test|docs/examples)/
|
||||
|
||||
# Packages excluded from unit tests: coverage exclusions + client/rpc (tested by test_cli)
|
||||
UNIT_EXCLUDE := /(test|docs/examples)/|/client/rpc$$
|
||||
|
||||
# Unit tests with coverage
|
||||
# Produces JSON for CI reporting and coverage profile for Codecov
|
||||
test_unit: test/bin/gotestsum $$(DEPS_GO)
|
||||
mkdir -p test/unit coverage
|
||||
rm -f test/unit/gotest.json coverage/unit_tests.coverprofile
|
||||
gotestsum $(GOTESTSUM_NOCOLOR) --jsonfile test/unit/gotest.json -- $(go-flags-with-tags) $(GOTFLAGS) -covermode=atomic -coverprofile=coverage/unit_tests.coverprofile -coverpkg=$$($(GOCC) list $(go-tags) ./... | grep -vE '$(COVERPKG_EXCLUDE)' | tr '\n' ',' | sed 's/,$$//') $$($(GOCC) list $(go-tags) ./... | grep -vE '$(UNIT_EXCLUDE)')
|
||||
.PHONY: test_unit
|
||||
|
||||
# CLI/integration tests (requires built binary in PATH)
|
||||
# Includes test/cli, test/integration, and client/rpc
|
||||
# Produces JSON for CI reporting
|
||||
# Override TEST_CLI_TIMEOUT for local development: make test_cli TEST_CLI_TIMEOUT=5m
|
||||
TEST_CLI_TIMEOUT ?= 10m
|
||||
test_cli: cmd/ipfs/ipfs test/bin/gotestsum $$(DEPS_GO)
|
||||
mkdir -p test/cli
|
||||
rm -f test/cli/cli-tests.json
|
||||
PATH="$(CURDIR)/cmd/ipfs:$(CURDIR)/test/bin:$$PATH" gotestsum $(GOTESTSUM_NOCOLOR) --jsonfile test/cli/cli-tests.json -- -v -timeout=$(TEST_CLI_TIMEOUT) ./test/cli/... ./test/integration/... ./client/rpc/...
|
||||
.PHONY: test_cli
|
||||
|
||||
# Example tests (docs/examples/kubo-as-a-library)
|
||||
# Tests against both published and current kubo versions
|
||||
# Uses timeout to ensure CI gets output before job-level timeout kills everything
|
||||
TEST_EXAMPLES_TIMEOUT ?= 2m
|
||||
test_examples:
|
||||
cd docs/examples/kubo-as-a-library && go test -v -timeout=$(TEST_EXAMPLES_TIMEOUT) ./... && cp go.mod go.mod.bak && cp go.sum go.sum.bak && (go mod edit -replace github.com/ipfs/kubo=./../../.. && go mod tidy && go test -v -timeout=$(TEST_EXAMPLES_TIMEOUT) ./...; ret=$$?; mv go.mod.bak go.mod; mv go.sum.bak go.sum; exit $$ret)
|
||||
.PHONY: test_examples
|
||||
|
||||
# Build kubo for all platforms from .github/build-platforms.yml
|
||||
test_go_build:
|
||||
bin/test-go-build-platforms
|
||||
.PHONY: test_go_build
|
||||
|
||||
test_go_short: GOTFLAGS += -test.short
|
||||
test_go_short: test_go_test
|
||||
.PHONY: test_go_short
|
||||
|
||||
test_go_race: GOTFLAGS += -race
|
||||
test_go_race: test_go_test
|
||||
.PHONY: test_go_race
|
||||
|
||||
test_go_expensive: test_go_test test_go_build
|
||||
.PHONY: test_go_expensive
|
||||
TEST_GO += test_go_expensive
|
||||
|
||||
# Check Go source formatting
|
||||
test_go_fmt:
|
||||
bin/test-go-fmt
|
||||
.PHONY: test_go_fmt
|
||||
TEST_GO += test_go_fmt
|
||||
|
||||
# Run golangci-lint (used by CI)
|
||||
test_go_lint: test/bin/golangci-lint
|
||||
golangci-lint run --timeout=3m ./...
|
||||
.PHONY: test_go_lint
|
||||
|
||||
test_go: $(TEST_GO)
|
||||
|
||||
# Version check is no longer needed - go.mod enforces minimum version
|
||||
.PHONY: check_go_version
|
||||
|
||||
TEST_GO := test_go_fmt test_unit test_cli test_examples
|
||||
TEST += $(TEST_GO)
|
||||
TEST_SHORT += test_go_fmt test_go_short
|
||||
TEST_SHORT += test_go_fmt test_unit
|
||||
|
||||
@ -2,7 +2,6 @@ package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
@ -21,11 +20,6 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// swarmPeersOutput is used to parse the JSON output of 'ipfs swarm peers --enc=json'
|
||||
type swarmPeersOutput struct {
|
||||
Peers []struct{} `json:"Peers"`
|
||||
}
|
||||
|
||||
func TestRoutingV1Server(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@ -209,11 +203,14 @@ func TestRoutingV1Server(t *testing.T) {
|
||||
c, err := client.New(node.GatewayURL())
|
||||
require.NoError(t, err)
|
||||
|
||||
// Try to get closest peers - should fail gracefully with an error
|
||||
// Try to get closest peers - should fail gracefully with an error.
|
||||
// Use 60-second timeout (server has 30s routing timeout).
|
||||
testCid, err := cid.Decode("QmUNLLsPACCz1vLxQVkXqqLX5R1X345qqfHbsf67hvA3Nn")
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = c.GetClosestPeers(context.Background(), testCid)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
||||
defer cancel()
|
||||
_, err = c.GetClosestPeers(ctx, testCid)
|
||||
require.Error(t, err)
|
||||
// All these routing types should indicate DHT is not available
|
||||
// The exact error message may vary based on implementation details
|
||||
@ -227,7 +224,7 @@ func TestRoutingV1Server(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("GetClosestPeers returns peers for self", func(t *testing.T) {
|
||||
t.Run("GetClosestPeers returns peers", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
routingTypes := []string{"auto", "autoclient", "dht", "dhtclient"}
|
||||
@ -246,47 +243,33 @@ func TestRoutingV1Server(t *testing.T) {
|
||||
node.StartDaemon()
|
||||
defer node.StopDaemon()
|
||||
|
||||
// Create client before waiting so we can probe DHT readiness
|
||||
c, err := client.New(node.GatewayURL())
|
||||
require.NoError(t, err)
|
||||
|
||||
// Query for closest peers to our own peer ID
|
||||
key := peer.ToCid(node.PeerID())
|
||||
|
||||
// Wait for node to connect to bootstrap peers and populate WAN DHT routing table
|
||||
minPeers := len(autoconf.FallbackBootstrapPeers)
|
||||
require.EventuallyWithT(t, func(t *assert.CollectT) {
|
||||
res := node.RunIPFS("swarm", "peers", "--enc=json")
|
||||
var output swarmPeersOutput
|
||||
err := json.Unmarshal(res.Stdout.Bytes(), &output)
|
||||
assert.NoError(t, err)
|
||||
peerCount := len(output.Peers)
|
||||
// Wait until we have at least minPeers connected
|
||||
assert.GreaterOrEqual(t, peerCount, minPeers,
|
||||
"waiting for at least %d bootstrap peers, currently have %d", minPeers, peerCount)
|
||||
}, 60*time.Second, time.Second)
|
||||
|
||||
// Wait for DHT to be ready by probing GetClosestPeers until it succeeds
|
||||
require.EventuallyWithT(t, func(t *assert.CollectT) {
|
||||
probeCtx, probeCancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer probeCancel()
|
||||
probeIter, probeErr := c.GetClosestPeers(probeCtx, key)
|
||||
if probeErr == nil {
|
||||
probeIter.Close()
|
||||
// Wait for WAN DHT routing table to be populated.
|
||||
// The server has a 30-second routing timeout, so we use 60 seconds
|
||||
// per request to allow for network latency while preventing hangs.
|
||||
// Total wait time is 2 minutes (locally passes in under 1 minute).
|
||||
var records []*types.PeerRecord
|
||||
require.EventuallyWithT(t, func(ct *assert.CollectT) {
|
||||
ctx, cancel := context.WithTimeout(t.Context(), 60*time.Second)
|
||||
defer cancel()
|
||||
resultsIter, err := c.GetClosestPeers(ctx, key)
|
||||
if !assert.NoError(ct, err) {
|
||||
return
|
||||
}
|
||||
assert.NoError(t, probeErr, "DHT should be ready to handle GetClosestPeers")
|
||||
records, err = iter.ReadAllResults(resultsIter)
|
||||
assert.NoError(ct, err)
|
||||
}, 2*time.Minute, 5*time.Second)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
||||
defer cancel()
|
||||
resultsIter, err := c.GetClosestPeers(ctx, key)
|
||||
require.NoError(t, err)
|
||||
|
||||
records, err := iter.ReadAllResults(resultsIter)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify we got some peers back from WAN DHT
|
||||
assert.NotEmpty(t, records, "should return some peers close to own peerid")
|
||||
require.NotEmpty(t, records, "should return peers close to own peerid")
|
||||
|
||||
// Per IPIP-0476, GetClosestPeers returns at most 20 peers
|
||||
assert.LessOrEqual(t, len(records), 20, "IPIP-0476 limits GetClosestPeers to 20 peers")
|
||||
|
||||
// Verify structure of returned records
|
||||
for _, record := range records {
|
||||
|
||||
@ -15,7 +15,7 @@ require (
|
||||
github.com/ipfs/iptb-plugins v0.5.1
|
||||
github.com/multiformats/go-multiaddr v0.16.1
|
||||
github.com/multiformats/go-multihash v0.2.3
|
||||
gotest.tools/gotestsum v1.12.3
|
||||
gotest.tools/gotestsum v1.13.0
|
||||
)
|
||||
|
||||
require (
|
||||
|
||||
@ -985,8 +985,8 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gotest.tools/gotestsum v1.12.3 h1:jFwenGJ0RnPkuKh2VzAYl1mDOJgbhobBDeL2W1iEycs=
|
||||
gotest.tools/gotestsum v1.12.3/go.mod h1:Y1+e0Iig4xIRtdmYbEV7K7H6spnjc1fX4BOuUhWw2Wk=
|
||||
gotest.tools/gotestsum v1.13.0 h1:+Lh454O9mu9AMG1APV4o0y7oDYKyik/3kBOiCqiEpRo=
|
||||
gotest.tools/gotestsum v1.13.0/go.mod h1:7f0NS5hFb0dWr4NtcsAsF0y1kzjEFfAil0HiBQJE03Q=
|
||||
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
|
||||
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
|
||||
honnef.co/go/tools v0.6.1 h1:R094WgE8K4JirYjBaOpz/AvTyUu/3wbmAoskKN/pxTI=
|
||||
|
||||
@ -2,7 +2,8 @@ include mk/header.mk
|
||||
|
||||
CLEAN += $(d)/gotest.json $(d)/gotest.junit.xml
|
||||
|
||||
$(d)/gotest.junit.xml: test/bin/gotestsum coverage/unit_tests.coverprofile
|
||||
# Convert gotest.json (produced by test_unit) to JUnit XML format
|
||||
$(d)/gotest.junit.xml: test/bin/gotestsum $(d)/gotest.json
|
||||
gotestsum --no-color --junitfile $@ --raw-command cat $(@D)/gotest.json
|
||||
|
||||
include mk/footer.mk
|
||||
|
||||
Loading…
Reference in New Issue
Block a user