a06f331eb8
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
166 lines
5.4 KiB
Go
166 lines
5.4 KiB
Go
package languages
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"github.com/zzet/gortex/internal/graph"
|
|
)
|
|
|
|
func TestDockerfile_FromEmitsImageNodes(t *testing.T) {
|
|
src := []byte(`FROM golang:1.22 AS builder
|
|
FROM alpine:3.18
|
|
`)
|
|
e := NewDockerfileExtractor()
|
|
result, err := e.Extract("app.dockerfile", src)
|
|
require.NoError(t, err)
|
|
|
|
nodes := nodesByID(result.Nodes)
|
|
|
|
// Two stage Image nodes — `builder` (alias) and `stage-1`
|
|
// (synthetic name for the second, unaliased FROM).
|
|
require.Contains(t, nodes, "image::stage::app.dockerfile::builder")
|
|
require.Contains(t, nodes, "image::stage::app.dockerfile::stage-1")
|
|
|
|
// Two base Image nodes.
|
|
require.Contains(t, nodes, "image::golang:1.22")
|
|
require.Contains(t, nodes, "image::alpine:3.18")
|
|
|
|
// Stage → base via EdgeDependsOn.
|
|
assert.True(t, hasEdgeBetween(result.Edges, graph.EdgeDependsOn,
|
|
"image::stage::app.dockerfile::builder", "image::golang:1.22"))
|
|
assert.True(t, hasEdgeBetween(result.Edges, graph.EdgeDependsOn,
|
|
"image::stage::app.dockerfile::stage-1", "image::alpine:3.18"))
|
|
}
|
|
|
|
func TestDockerfile_MultiStageChain(t *testing.T) {
|
|
src := []byte(`FROM golang:1.22 AS builder
|
|
FROM builder AS shrunk
|
|
FROM alpine:3.18
|
|
COPY --from=shrunk /out /usr/bin/app
|
|
`)
|
|
e := NewDockerfileExtractor()
|
|
result, err := e.Extract("multi.dockerfile", src)
|
|
require.NoError(t, err)
|
|
|
|
// `shrunk` stage depends on the prior `builder` stage.
|
|
assert.True(t, hasEdgeBetween(result.Edges, graph.EdgeDependsOn,
|
|
"image::stage::multi.dockerfile::shrunk",
|
|
"image::stage::multi.dockerfile::builder"),
|
|
"shrunk should depend on builder via the stage chain")
|
|
}
|
|
|
|
func TestDockerfile_EnvEmitsConfigKeyAndUsesEnv(t *testing.T) {
|
|
src := []byte(`FROM ubuntu:22.04
|
|
ENV DATABASE_URL=postgres://localhost
|
|
ENV LOG_LEVEL=info
|
|
ARG BUILD_REV=dev
|
|
`)
|
|
e := NewDockerfileExtractor()
|
|
result, err := e.Extract("svc.dockerfile", src)
|
|
require.NoError(t, err)
|
|
|
|
nodes := nodesByID(result.Nodes)
|
|
stage := "image::stage::svc.dockerfile::stage-0"
|
|
|
|
for _, key := range []string{"DATABASE_URL", "LOG_LEVEL", "BUILD_REV"} {
|
|
id := "cfg::env::" + key
|
|
require.Contains(t, nodes, id)
|
|
assert.Equal(t, graph.KindConfigKey, nodes[id].Kind)
|
|
assert.True(t, hasEdgeBetween(result.Edges, graph.EdgeUsesEnv, stage, id),
|
|
"stage should declare uses_env for %q", key)
|
|
}
|
|
}
|
|
|
|
func TestDockerfile_ExposeEmitsExposes(t *testing.T) {
|
|
src := []byte(`FROM nginx:1.25
|
|
EXPOSE 80 443/tcp
|
|
EXPOSE 5353/udp
|
|
`)
|
|
e := NewDockerfileExtractor()
|
|
result, err := e.Extract("web.dockerfile", src)
|
|
require.NoError(t, err)
|
|
|
|
stage := "image::stage::web.dockerfile::stage-0"
|
|
assert.True(t, hasEdgeBetween(result.Edges, graph.EdgeExposes, stage, "port::tcp::80"))
|
|
assert.True(t, hasEdgeBetween(result.Edges, graph.EdgeExposes, stage, "port::tcp::443"))
|
|
assert.True(t, hasEdgeBetween(result.Edges, graph.EdgeExposes, stage, "port::udp::5353"))
|
|
}
|
|
|
|
func TestDockerfile_PreFromArgBindsToFile(t *testing.T) {
|
|
// Global build args declared before any FROM. The legacy
|
|
// KindVariable + KindConfigKey nodes still fire; the
|
|
// EdgeUsesEnv from the file (since no stage exists yet) is a
|
|
// regression guard for the pre-FROM cursor branch.
|
|
src := []byte(`ARG VERSION=1.0
|
|
FROM scratch
|
|
`)
|
|
e := NewDockerfileExtractor()
|
|
result, err := e.Extract("global.dockerfile", src)
|
|
require.NoError(t, err)
|
|
|
|
require.Contains(t, nodesByID(result.Nodes), "cfg::env::VERSION")
|
|
}
|
|
|
|
// TestDockerfile_ImageIDSharedAcrossExtractors validates the
|
|
// invariant that a Dockerfile FROM and a K8s container.image with
|
|
// the same image ref produce the same KindImage node — so a
|
|
// `find_usages` walk on the Image node returns both the Dockerfile
|
|
// stage and the K8s Resource.
|
|
func TestDockerfile_ImageIDSharedAcrossExtractors(t *testing.T) {
|
|
dockerfileSrc := []byte("FROM ghcr.io/acme/api:1.2.3 AS api\n")
|
|
dRes, err := NewDockerfileExtractor().Extract("svc/Dockerfile", dockerfileSrc)
|
|
require.NoError(t, err)
|
|
|
|
k8sSrc := []byte(`apiVersion: apps/v1
|
|
kind: Deployment
|
|
metadata:
|
|
name: api
|
|
namespace: prod
|
|
spec:
|
|
template:
|
|
spec:
|
|
containers:
|
|
- name: api
|
|
image: ghcr.io/acme/api:1.2.3
|
|
`)
|
|
kRes, err := NewYAMLExtractor().Extract("k8s/api.yaml", k8sSrc)
|
|
require.NoError(t, err)
|
|
|
|
dHas := false
|
|
for _, n := range dRes.Nodes {
|
|
if n.ID == "image::ghcr.io/acme/api:1.2.3" {
|
|
dHas = true
|
|
}
|
|
}
|
|
kHas := false
|
|
for _, n := range kRes.Nodes {
|
|
if n.ID == "image::ghcr.io/acme/api:1.2.3" {
|
|
kHas = true
|
|
}
|
|
}
|
|
assert.True(t, dHas, "Dockerfile should emit shared image node")
|
|
assert.True(t, kHas, "K8s should emit shared image node")
|
|
}
|
|
|
|
// TestDockerfile_ConfigKeyIDSharedWithGoOsGetenv validates the
|
|
// cross-ref invariant: a Dockerfile ENV NAME and a Go os.Getenv("NAME")
|
|
// emit the same `cfg::env::NAME` node ID. We only check the
|
|
// Dockerfile side here — the Go side is exercised by go_configs_test.go
|
|
// which already asserts the same ID convention. The pairing across
|
|
// the two emitters is the load-bearing property tested by this case.
|
|
func TestDockerfile_ConfigKeyIDSharedWithGoOsGetenv(t *testing.T) {
|
|
src := []byte("FROM scratch\nENV DATABASE_URL=postgres://...\n")
|
|
res, err := NewDockerfileExtractor().Extract("Dockerfile", src)
|
|
require.NoError(t, err)
|
|
|
|
found := false
|
|
for _, n := range res.Nodes {
|
|
if n.ID == "cfg::env::DATABASE_URL" && n.Kind == graph.KindConfigKey {
|
|
found = true
|
|
}
|
|
}
|
|
assert.True(t, found, "Dockerfile ENV must produce cfg::env::<NAME> matching os.Getenv convention")
|
|
}
|