harn-vm 0.8.79

Async bytecode virtual machine for the Harn programming language
Documentation
1
2
3
[[CALL]]
edit({ action: "create", path: "internal/manifest/parser_extra_test.go", content: <<EOF\npackage manifest\n\nimport (\n\t\"strings\"\n\t\"testing\"\n)\n\n// TestParse_EmptyManifest verifies that a manifest with no services section\n// parses successfully and applyDefaults does not crash on a nil map.\nfunc TestParse_EmptyManifest(t *testing.T) {\n\tm := assertParseOK(t, `version: \"1\"\nservices:`)\n\tassertServiceCount(t, m, 0)\n}\n\n// TestParse_EmptyServicesBlock verifies that an explicit empty services map\n// yields zero services (distinct from the nil case).\nfunc TestParse_EmptyServicesBlock(t *testing.T) {\n\tm := assertParseOK(t, `version: \"1\"\nservices: {}`)\n\tassertServiceCount(t, m, 0)\n}\n\n// TestParse_InvalidYAML exercises several malformed YAML inputs and expects\n// a non-nil error for each.\nfunc TestParse_InvalidYAML(t *testing.T) {\n\tinputs := []string{\n\t\t`{{{not yaml`,\n\t\t`version: \"1`\n\t\t\t`services:`,\n\t\t`- list at root`,\n\t\t`version: \"1\"\nservices:\n  - not a map`,\n\t\t`version: [1, 2, 3]`,\n\t}\n\tfor _, input := range inputs {\n\t\tt.Run(\"invalid yaml: \"+strings.ReplaceAll(input[:min(20, len(input))], \"\\n\", \" \"), func(t *testing.T) {\n\t\t\t_, err := Parse(strings.NewReader(input))\n\t\t\tif err == nil {\n\t\t\t\tt.Fatalf(\"expected error for input %q, got nil\", input)\n\t\t\t}\n\t\t})\n\t}\n}\n\n// TestParse_MissingRequiredImage verifies that a service without an image\n// field is accepted by the parser (Parse does not enforce required fields).\nfunc TestParse_MissingRequiredImage(t *testing.T) {\n\tm := assertParseOK(t, `version: \"1\"\nservices:\n  orphan:\n    ports:\n      - \"8080:80\"`)\n\tassertServiceCount(t, m, 1)\n\tif m.Services[\"orphan\"].Image != \"\" {\n\t\tt.Errorf(\"expected empty image, got %q\", m.Services[\"orphan\"].Image)\n\t}\n}\n\n// TestParse_DuplicateServiceNames verifies that duplicate keys in the YAML\n// services map are handled correctly. In YAML, duplicate keys produce the\n// last value (per the spec), so the parser should contain exactly one entry\n// for the duplicated service name.\nfunc TestParse_DuplicateServiceNames(t *testing.T) {\n\tm := assertParseOK(t, `version: \"1\"\nservices:\n  web:\n    image: nginx:latest\n  web:\n    image: apache:latest`)\n\tassertServiceCount(t, m, 1)\n\tif m.Services[\"web\"].Image != \"apache:latest\" {\n\t\tt.Errorf(\"expected image %q, got %q\", \"apache:latest\", m.Services[\"web\"].Image)\n\t}\n}\n\n// TestParse_PortValidation exercises port string parsing at the parser level.\n// The parser does not validate ports; that is left to the validator. Here we\n// confirm that the parser simply stores the raw port strings.\nfunc TestParse_PortValidation(t *testing.T) {\n\ttests := []struct {\n\t\tname  string\n\t\tinput string\n\t\tcheck func(t *testing.T, m *Manifest)\n\t}{\n\t\t{\n\t\t\tname: \"valid port mapping\",\n\t\t\tinput: `version: \"1\"\nservices:\n  web:\n    image: nginx\n    ports:\n      - \"8080:80\"\n      - \"443:443\"`,\n\t\t\tcheck: func(t *testing.T, m *Manifest) {\n\t\t\t\tsvc := m.Services[\"web\"]\n\t\t\t\tif len(svc.Ports) != 2 {\n\t\t\t\t\tt.Fatalf(\"expected 2 ports, got %d\", len(svc.Ports))\n\t\t\t\t}\n\t\t\t\tassertStringEqual(t, \"ports[0]\", svc.Ports[0], \"8080:80\")\n\t\t\t\tassertStringEqual(t, \"ports[1]\", svc.Ports[1], \"443:443\")\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"invalid port format stored as-is\",\n\t\t\tinput: `version: \"1\"\nservices:\n  web:\n    image: nginx\n    ports:\n      - \"not-a-port\"`,\n\t\t\tcheck: func(t *testing.T, m *Manifest) {\n\t\t\t\tsvc := m.Services[\"web\"]\n\t\t\t\tif len(svc.Ports) != 1 {\n\t\t\t\t\tt.Fatalf(\"expected 1 port, got %d\", len(svc.Ports))\n\t\t\t\t}\n\t\t\t\tassertStringEqual(t, \"ports[0]\", svc.Ports[0], \"not-a-port\")\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"port with protocol suffix\",\n\t\t\tinput: `version: \"1\"\nservices:\n  web:\n    image: nginx\n    ports:\n      - \"8080:80/tcp\"`,\n\t\t\tcheck: func(t *testing.T, m *Manifest) {\n\t\t\t\tsvc := m.Services[\"web\"]\n\t\t\t\tif len(svc.Ports) != 1 {\n\t\t\t\t\tt.Fatalf(\"expected 1 port, got %d\", len(svc.Ports))\n\t\t\t\t}\n\t\t\t\tassertStringEqual(t, \"ports[0]\", svc.Ports[0], \"8080:80/tcp\")\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"empty port list\",\n\t\t\tinput: `version: \"1\"\nservices:\n  web:\n    image: nginx\n    ports: []`,\n\t\t\tcheck: func(t *testing.T, m *Manifest) {\n\t\t\t\tsvc := m.Services[\"web\"]\n\t\t\t\tif len(svc.Ports) != 0 {\n\t\t\t\t\tt.Fatalf(\"expected 0 ports, got %d\", len(svc.Ports))\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tm := assertParseOK(t, tt.input)\n\t\t\tif tt.check != nil {\n\t\t\t\ttt.check(t, m)\n\t\t\t}\n\t\t})\n\t}\n}\n\n// TestParse_ValidationErrors verifies that Validate catches the edge cases\n// that Parse intentionally does not: missing image, missing required fields,\n// and port format issues.\nfunc TestParse_ValidationErrors(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tinput    string\n\t\twantErrs []string\n\t}{\n\t\t{\n\t\t\tname: \"missing image\",\n\t\t\tinput: `version: \"1\"\nservices:\n  web:\n    ports:\n      - \"8080:80\"`,\n\t\t\twantErrs: []string{\"services.web.image\"},\n\t\t},\n\t\t{\n\t\t\tname: \"missing version\",\n\t\t\tinput: `services:\n  web:\n    image: nginx`,\n\t\t\twantErrs: []string{\"services\"},\n\t\t},\n\t\t{\n\t\t\tname: \"invalid port format\",\n\t\t\tinput: `version: \"1\"\nservices:\n  web:\n    image: nginx\n    ports:\n      - \"not-a-port\"`,\n\t\t\twantErrs: []string{\"services.web.ports[0]\"},\n\t\t},\n\t\t{\n\t\t\tname: \"replicas less than 1\",\n\t\t\tinput: `version: \"1\"\nservices:\n  web:\n    image: nginx\n    replicas: 0`,\n\t\t\twantErrs: []string{\"services.web.replicas\"},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tm := assertParseOK(t, tt.input)\n\t\t\terr := Validate(m)\n\t\t\tif err == nil {\n\t\t\t\tt.Fatalf(\"expected validation error, got nil\")\n\t\t\t}\n\t\t\tve, ok := err.(*ValidationError)\n\t\t\tif !ok {\n\t\t\t\tt.Fatalf(\"expected *ValidationError, got %T\", err)\n\t\t\t}\n\t\t\tfor _, want := range tt.wantErrs {\n\t\t\t\tfound := false\n\t\t\t\tfor _, fe := range ve.Errors {\n\t\t\t\t\tif fe.Field == want {\n\t\t\t\t\t\tfound = true\n\t\t\t\t\t\tbreak\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif !found {\n\t\t\t\t\tt.Errorf(\"expected error field %q in %v\", want, ve.Errors)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\n// TestParse_MissingRequiredFields verifies that the parser accepts a manifest\n// with missing optional fields and applies defaults correctly.\nfunc TestParse_MissingRequiredFields(t *testing.T) {\n\tm := assertParseOK(t, `version: \"1\"\nservices:\n  minimal:\n    image: busybox:latest`)\n\tassertServiceCount(t, m, 1)\n\tsvc := m.Services[\"minimal\"]\n\tassertIntEqual(t, \"replicas\", svc.Replicas, 1)\n\tassertStringEqual(t, \"restart\", string(svc.Restart), string(RestartNever))\n\tif svc.Ports != nil && len(svc.Ports) != 0 {\n\t\tt.Errorf(\"expected nil/empty ports, got %v\", svc.Ports)\n\t}\n\tif svc.Environment != nil && len(svc.Environment) != 0 {\n\t\tt.Errorf(\"expected nil/empty environment, got %v\", svc.Environment)\n\t}\n}\n\n// TestParse_CompleteManifest exercises a realistic manifest with all major\n// fields populated to ensure nothing is silently dropped during parsing.\nfunc TestParse_CompleteManifest(t *testing.T) {\n\tinput := `version: \"1\"\nservices:\n  web:\n    image: nginx:1.25\n    ports:\n      - \"8080:80\"\n      - \"8443:443\"\n    environment:\n      NGINX_HOST: localhost\n      NGINX_PORT: \"80\"\n    depends_on:\n      - api\n    health_check:\n      http: http://localhost:80/health\n      interval: 15s\n      timeout: 3s\n      retries: 4\n    replicas: 2\n    restart: always\n    volumes:\n      - ./data:/data\n    command:\n      - nginx\n      - -g\n      - \"daemon off;\"\n    entrypoint:\n      - /usr/local/bin/start.sh\n    labels:\n      app: web\n      env: prod\nnetworks:\n  default:\n    driver: bridge\nvolumes:\n  data:\n    driver: local`\n\tm := assertParseOK(t, input)\n\tassertServiceCount(t, m, 1)\n\n\tweb := m.Services[\"web\"]\n\tassertStringEqual(t, \"web.image\", web.Image, \"nginx:1.25\")\n\tassertIntEqual(t, \"web.replicas\", web.Replicas, 2)\n\tassertStringEqual(t, \"web.restart\", string(web.Restart), string(RestartAlways))\n\tassertIntEqual(t, \"web.ports\", len(web.Ports), 2)\n\tassertStringEqual(t, \"web.ports[0]\", web.Ports[0], \"8080:80\")\n\tassertStringEqual(t, \"web.ports[1]\", web.Ports[1], \"8443:443\")\n\tassertStringEqual(t, \"web.environment[NGINX_HOST]\", web.Environment[\"NGINX_HOST\"], \"localhost\")\n\tassertStringEqual(t, \"web.environment[NGINX_PORT]\", web.Environment[\"NGINX_PORT\"], \"80\")\n\tassertIntEqual(t, \"web.depends_on\", len(web.DependsOn), 1)\n\tassertStringEqual(t, \"web.depends_on[0]\", web.DependsOn[0], \"api\")\n\n\thc := web.HealthCheck\n\tassertHealthCheckNotNil(t, hc)\n\tassertStringEqual(t, \"web.health_check.http\", hc.HTTP, \"http://localhost:80/health\")\n\tassertDurationEqual(t, \"web.health_check.interval\", hc.Interval, 15*time.Second)\n\tassertDurationEqual(t, \"web.health_check.timeout\", hc.Timeout, 3*time.Second)\n\tassertIntEqual(t, \"web.health_check.retries\", hc.Retries, 4)\n\n\tassertIntEqual(t, \"web.volumes\", len(web.Volumes), 1)\n\tassertStringEqual(t, \"web.volumes[0]\", web.Volumes[0], \"./data:/data\")\n\tassertIntEqual(t, \"web.command\", len(web.Command), 3)\n\tassertStringEqual(t, \"web.command[0]\", web.Command[0], \"nginx\")\n\tassertStringEqual(t, \"web.entrypoint\", len(web.Entrypoint), 1)\n\tassertStringEqual(t, \"web.entrypoint[0]\", web.Entrypoint[0], \"/usr/local/bin/start.sh\")\n\tassertStringEqual(t, \"web.labels[app]\", web.Labels[\"app\"], \"web\")\n\tassertStringEqual(t, \"web.labels[env]\", web.Labels[\"env\"], \"prod\")\n\n\tif len(m.Networks) != 1 {\n\t\tt.Fatalf(\"expected 1 network, got %d\", len(m.Networks))\n\t}\n\tif m.Networks[\"default\"].Driver != \"bridge\" {\n\t\tt.Errorf(\"expected network driver %q, got %q\", \"bridge\", m.Networks[\"default\"].Driver)\n\t}\n\n\tif len(m.Volumes) != 1 {\n\t\tt.Fatalf(\"expected 1 volume, got %d\", len(m.Volumes))\n\t}\n\tif m.Volumes[\"data\"].Driver != \"local\" {\n\t\tt.Errorf(\"expected volume driver %q, got %q\", \"local\", m.Volumes[\"data\"].Driver)\n\t}\n}\n\n// min returns the smaller of a and b (helper to avoid importing \"math\").\nfunc min(a, b int) int {\n\tif a < b {\n\t\treturn a\n\t}\n\treturn b\n}\nEOF, overwrite: true })
[[/CALL]]