From afaf3fe8dab6f61ef140f31a6466fed509f6a606 Mon Sep 17 00:00:00 2001 From: joshsiv-mulga Date: Mon, 18 May 2026 22:44:13 +1000 Subject: [PATCH] test(e2e): parallelise hot phases + tune fast-path polls Squash merge of feat/e2e-parallel-quickwins (mulga-siv-96). Four quick wins from decomposing run-e2e.sh into Go phase subtests: - Phase 8 negatives (single/negative_test.go): t.Parallel() inside each of the 18 read-only error-path sub-tests. ~5-10x speedup. - IAM Phase 4 policies (single/iam_test.go): EC2ReadOnly stays seeded sequentially (populates fix.IAMAdminAccount); the other four policies fan out as siblings under a wrapping create_policies_parallel t.Run. ~5x on the Phase 4 sub-step. - Volume + snapshot fast paths (single/{volume,snapshot}_test.go): pass harness.WithPoll(500ms) on the five state transitions that finish in <1s on local QEMU. Halves Phase 5b / 5c wall-clock. - Cert per-IP loop (cert/cert_test.go): t.Parallel() inside the 4 inner t.Run(ip, ...) blocks. Cuts cert phase by len(nodes). No harness changes, no Fixture changes, no test-execution-order changes. mulga-siv-96. --- tests/e2e/cert/cert_test.go | 4 ++ tests/e2e/single/iam_test.go | 71 +++++++++++++++++++------------ tests/e2e/single/negative_test.go | 18 ++++++++ tests/e2e/single/snapshot_test.go | 6 ++- tests/e2e/single/volume_test.go | 9 ++-- 5 files changed, 75 insertions(+), 33 deletions(-) diff --git a/tests/e2e/cert/cert_test.go b/tests/e2e/cert/cert_test.go index ba559c435..3fe66d4e5 100644 --- a/tests/e2e/cert/cert_test.go +++ b/tests/e2e/cert/cert_test.go @@ -37,6 +37,7 @@ func TestCertIssuance(t *testing.T) { for _, ip := range env.NodeIPs { ip := ip t.Run(ip, func(t *testing.T) { + t.Parallel() cert := loadCert(t, env, ip, artifacts) assert.True(t, harness.CertHasIPSAN(cert, ip), "cert for %s missing IP SAN %s (got IPs=%v DNS=%v)", @@ -69,6 +70,7 @@ func TestCertIssuance(t *testing.T) { for _, ip := range env.ServiceIPs { ip := ip t.Run(ip, func(t *testing.T) { + t.Parallel() url := fmt.Sprintf("https://%s:%d/", ip, env.AWSGWPort) code, _, err := harness.HTTPSGet(url, pool, env.DefaultTimeout) require.NoError(t, err, "TLS handshake to %s", url) @@ -103,6 +105,7 @@ func TestCertIssuance(t *testing.T) { for _, ip := range env.ServiceIPs { ip := ip t.Run(ip, func(t *testing.T) { + t.Parallel() require.NoError(t, harness.OpenSSLVerify(t, caPath, ip, env.AWSGWPort)) }) } @@ -152,6 +155,7 @@ func TestCertIssuance(t *testing.T) { for _, ip := range env.ServiceIPs { ip := ip t.Run(ip, func(t *testing.T) { + t.Parallel() served, err := harness.FetchServedCert(ip, env.AWSGWPort, env.DefaultTimeout) require.NoError(t, err) assert.Contains(t, served.Issuer.CommonName, "Spinifex Local CA", diff --git a/tests/e2e/single/iam_test.go b/tests/e2e/single/iam_test.go index 1b58bd7ac..a6e2dd00b 100644 --- a/tests/e2e/single/iam_test.go +++ b/tests/e2e/single/iam_test.go @@ -388,35 +388,50 @@ func phaseIAM4_PolicyCRUD(t *testing.T, fix *Fixture) { fix.IAMAdminAccount = iamAccountFromARN(t, ec2roArn) harness.Detail(t, "policy", iamPolicyEC2ReadOnly, "arn", ec2roArn, "account", fix.IAMAdminAccount) - harness.Step(t, "create-policy %s path=%s", iamPolicyFullAdmin, iamPolicyFullAdminPath) - _, err = fix.AWS.IAM.CreatePolicy(&iam.CreatePolicyInput{ - PolicyName: aws.String(iamPolicyFullAdmin), - Path: aws.String(iamPolicyFullAdminPath), - Description: aws.String("Full access to all services"), - PolicyDocument: aws.String(iamDocFullAdmin), + // EC2ReadOnly is seeded sequentially above because it populates + // fix.IAMAdminAccount (read by Phase 5). The remaining four policies + // have no cross-dependency, so fan them out in parallel under a + // wrapping t.Run that blocks until all four complete. + t.Run("create_policies_parallel", func(t *testing.T) { + t.Run(iamPolicyFullAdmin, func(t *testing.T) { + t.Parallel() + harness.Step(t, "create-policy %s path=%s", iamPolicyFullAdmin, iamPolicyFullAdminPath) + _, err := fix.AWS.IAM.CreatePolicy(&iam.CreatePolicyInput{ + PolicyName: aws.String(iamPolicyFullAdmin), + Path: aws.String(iamPolicyFullAdminPath), + Description: aws.String("Full access to all services"), + PolicyDocument: aws.String(iamDocFullAdmin), + }) + require.NoError(t, err, "create-policy %s", iamPolicyFullAdmin) + }) + t.Run(iamPolicyDenyTerminate, func(t *testing.T) { + t.Parallel() + harness.Step(t, "create-policy %s (mixed Allow+Deny)", iamPolicyDenyTerminate) + _, err := fix.AWS.IAM.CreatePolicy(&iam.CreatePolicyInput{ + PolicyName: aws.String(iamPolicyDenyTerminate), + PolicyDocument: aws.String(iamDocDenyTerminate), + }) + require.NoError(t, err, "create-policy %s", iamPolicyDenyTerminate) + }) + t.Run(iamPolicyIAMReadOnly, func(t *testing.T) { + t.Parallel() + harness.Step(t, "create-policy %s", iamPolicyIAMReadOnly) + _, err := fix.AWS.IAM.CreatePolicy(&iam.CreatePolicyInput{ + PolicyName: aws.String(iamPolicyIAMReadOnly), + PolicyDocument: aws.String(iamDocIAMReadOnly), + }) + require.NoError(t, err, "create-policy %s", iamPolicyIAMReadOnly) + }) + t.Run(iamPolicyEC2DescribeAll, func(t *testing.T) { + t.Parallel() + harness.Step(t, "create-policy %s (wildcard)", iamPolicyEC2DescribeAll) + _, err := fix.AWS.IAM.CreatePolicy(&iam.CreatePolicyInput{ + PolicyName: aws.String(iamPolicyEC2DescribeAll), + PolicyDocument: aws.String(iamDocEC2DescribeAll), + }) + require.NoError(t, err, "create-policy %s", iamPolicyEC2DescribeAll) + }) }) - require.NoError(t, err, "create-policy %s", iamPolicyFullAdmin) - - harness.Step(t, "create-policy %s (mixed Allow+Deny)", iamPolicyDenyTerminate) - _, err = fix.AWS.IAM.CreatePolicy(&iam.CreatePolicyInput{ - PolicyName: aws.String(iamPolicyDenyTerminate), - PolicyDocument: aws.String(iamDocDenyTerminate), - }) - require.NoError(t, err, "create-policy %s", iamPolicyDenyTerminate) - - harness.Step(t, "create-policy %s", iamPolicyIAMReadOnly) - _, err = fix.AWS.IAM.CreatePolicy(&iam.CreatePolicyInput{ - PolicyName: aws.String(iamPolicyIAMReadOnly), - PolicyDocument: aws.String(iamDocIAMReadOnly), - }) - require.NoError(t, err, "create-policy %s", iamPolicyIAMReadOnly) - - harness.Step(t, "create-policy %s (wildcard)", iamPolicyEC2DescribeAll) - _, err = fix.AWS.IAM.CreatePolicy(&iam.CreatePolicyInput{ - PolicyName: aws.String(iamPolicyEC2DescribeAll), - PolicyDocument: aws.String(iamDocEC2DescribeAll), - }) - require.NoError(t, err, "create-policy %s", iamPolicyEC2DescribeAll) harness.Step(t, "create-policy duplicate (expect EntityAlreadyExists)") harness.ExpectError(t, "EntityAlreadyExists", func() error { diff --git a/tests/e2e/single/negative_test.go b/tests/e2e/single/negative_test.go index c136eeaa6..96d876170 100644 --- a/tests/e2e/single/negative_test.go +++ b/tests/e2e/single/negative_test.go @@ -51,6 +51,7 @@ func phase8_NegativeErrorPaths(t *testing.T, fix *Fixture) { // 8a: RunInstances with malformed AMI ID (missing ami- prefix). t.Run("8a_InvalidAMIIDMalformed", func(t *testing.T) { + t.Parallel() harness.Step(t, "run-instances image-id=notanami") out, err := fix.AWS.EC2.RunInstances(&ec2.RunInstancesInput{ ImageId: aws.String("notanami"), @@ -65,6 +66,7 @@ func phase8_NegativeErrorPaths(t *testing.T, fix *Fixture) { // 8b: RunInstances with invalid instance type. t.Run("8b_InvalidInstanceType", func(t *testing.T) { + t.Parallel() harness.Step(t, "run-instances instance-type=x99.superlarge") out, err := fix.AWS.EC2.RunInstances(&ec2.RunInstancesInput{ ImageId: aws.String(fix.AMIID), @@ -79,6 +81,7 @@ func phase8_NegativeErrorPaths(t *testing.T, fix *Fixture) { // 8c: AttachVolume on the already-attached root volume. t.Run("8c_VolumeInUse", func(t *testing.T) { + t.Parallel() harness.Step(t, "attach-volume %s (root, in-use)", fix.RootVolumeID) _, err := fix.AWS.EC2.AttachVolume(&ec2.AttachVolumeInput{ VolumeId: aws.String(fix.RootVolumeID), @@ -90,6 +93,7 @@ func phase8_NegativeErrorPaths(t *testing.T, fix *Fixture) { // 8d: DetachVolume against the boot/root volume — explicitly disallowed. t.Run("8d_DetachRootForbidden", func(t *testing.T) { + t.Parallel() harness.Step(t, "detach-volume %s (root)", fix.RootVolumeID) _, err := fix.AWS.EC2.DetachVolume(&ec2.DetachVolumeInput{ VolumeId: aws.String(fix.RootVolumeID), @@ -100,6 +104,7 @@ func phase8_NegativeErrorPaths(t *testing.T, fix *Fixture) { // 8e: DeleteSnapshot on a non-existent snapshot id. t.Run("8e_DeleteSnapshotNotFound", func(t *testing.T) { + t.Parallel() harness.Step(t, "delete-snapshot snap-nonexistent000000") _, err := fix.AWS.EC2.DeleteSnapshot(&ec2.DeleteSnapshotInput{ SnapshotId: aws.String("snap-nonexistent000000"), @@ -113,6 +118,7 @@ func phase8_NegativeErrorPaths(t *testing.T, fix *Fixture) { // here we accept either of the two canonical codes and surface anything // else so we notice regressions in the gateway error envelope. t.Run("8f_InvalidActionRawHTTP", func(t *testing.T) { + t.Parallel() harness.Step(t, "POST Action=DescribeFakeThings") status, body, code := harness.PostAWSAction(t, fix.Env, fix.AWS, "DescribeFakeThings", nil) @@ -128,6 +134,7 @@ func phase8_NegativeErrorPaths(t *testing.T, fix *Fixture) { // 8g: RunInstances with a well-formed but non-existent AMI id. t.Run("8g_InvalidAMIIDNotFound", func(t *testing.T) { + t.Parallel() harness.Step(t, "run-instances image-id=ami-0000000000000dead") out, err := fix.AWS.EC2.RunInstances(&ec2.RunInstancesInput{ ImageId: aws.String("ami-0000000000000dead"), @@ -142,6 +149,7 @@ func phase8_NegativeErrorPaths(t *testing.T, fix *Fixture) { // 8h: RunInstances with non-existent key pair name. t.Run("8h_InvalidKeyPairNotFound", func(t *testing.T) { + t.Parallel() harness.Step(t, "run-instances key-name=nonexistent-key-xyz") out, err := fix.AWS.EC2.RunInstances(&ec2.RunInstancesInput{ ImageId: aws.String(fix.AMIID), @@ -156,6 +164,7 @@ func phase8_NegativeErrorPaths(t *testing.T, fix *Fixture) { // 8i: DeleteVolume on an unknown volume id. t.Run("8i_DeleteVolumeNotFound", func(t *testing.T) { + t.Parallel() harness.Step(t, "delete-volume vol-0000000000000dead") _, err := fix.AWS.EC2.DeleteVolume(&ec2.DeleteVolumeInput{ VolumeId: aws.String("vol-0000000000000dead"), @@ -165,6 +174,7 @@ func phase8_NegativeErrorPaths(t *testing.T, fix *Fixture) { // 8j: CreateKeyPair re-using test-key-1 (still present from Phase 3). t.Run("8j_CreateKeyPairDuplicate", func(t *testing.T) { + t.Parallel() harness.Step(t, "create-key-pair %s (duplicate)", existingKey) out, err := fix.AWS.EC2.CreateKeyPair(&ec2.CreateKeyPairInput{ KeyName: aws.String(existingKey), @@ -184,6 +194,7 @@ func phase8_NegativeErrorPaths(t *testing.T, fix *Fixture) { // 8k: ImportKeyPair re-using test-key-1. t.Run("8k_ImportKeyPairDuplicate", func(t *testing.T) { + t.Parallel() harness.Step(t, "import-key-pair %s (duplicate)", existingKey) _, err := fix.AWS.EC2.ImportKeyPair(&ec2.ImportKeyPairInput{ KeyName: aws.String(existingKey), @@ -194,6 +205,7 @@ func phase8_NegativeErrorPaths(t *testing.T, fix *Fixture) { // 8l: ImportKeyPair with garbage public-key material. t.Run("8l_InvalidKeyFormat", func(t *testing.T) { + t.Parallel() const badKeyName = "bad-format-key" harness.Step(t, "import-key-pair %s (bad material)", badKeyName) _, err := fix.AWS.EC2.ImportKeyPair(&ec2.ImportKeyPairInput{ @@ -214,6 +226,7 @@ func phase8_NegativeErrorPaths(t *testing.T, fix *Fixture) { // 8m: DescribeVolumes filtered by a non-existent volume id. t.Run("8m_DescribeVolumesNotFound", func(t *testing.T) { + t.Parallel() harness.Step(t, "describe-volumes vol-0000000000000dead") _, err := fix.AWS.EC2.DescribeVolumes(&ec2.DescribeVolumesInput{ VolumeIds: []*string{aws.String("vol-0000000000000dead")}, @@ -223,6 +236,7 @@ func phase8_NegativeErrorPaths(t *testing.T, fix *Fixture) { // 8n: DescribeImages filtered by a non-existent AMI id. t.Run("8n_DescribeImagesNotFound", func(t *testing.T) { + t.Parallel() harness.Step(t, "describe-images ami-0000000000000dead") _, err := fix.AWS.EC2.DescribeImages(&ec2.DescribeImagesInput{ ImageIds: []*string{aws.String("ami-0000000000000dead")}, @@ -234,6 +248,7 @@ func phase8_NegativeErrorPaths(t *testing.T, fix *Fixture) { // Stored in fix.CustomAMIID, but the duplicate-name check is by name, not // id — assert via the well-known constant from Phase 5e. t.Run("8o_CreateImageDuplicateName", func(t *testing.T) { + t.Parallel() harness.Step(t, "create-image name=%s (duplicate)", customAMIName) out, err := fix.AWS.EC2.CreateImage(&ec2.CreateImageInput{ InstanceId: aws.String(fix.InstanceID), @@ -256,6 +271,7 @@ func phase8_NegativeErrorPaths(t *testing.T, fix *Fixture) { // 8p: DeleteKeyPair on an unknown key must succeed (idempotent — matches // AWS). This is the only positive case in Phase 8. t.Run("8p_DeleteKeyPairIdempotent", func(t *testing.T) { + t.Parallel() const missingKey = "nonexistent-key-99999" harness.Step(t, "delete-key-pair %s (idempotent)", missingKey) _, err := fix.AWS.EC2.DeleteKeyPair(&ec2.DeleteKeyPairInput{ @@ -269,6 +285,7 @@ func phase8_NegativeErrorPaths(t *testing.T, fix *Fixture) { // stopped KV view, so the error surfaces as NotFound rather than // IncorrectInstanceState. Matches bash expectation. t.Run("8q_ModifyAttributeOnRunning", func(t *testing.T) { + t.Parallel() harness.Step(t, "modify-instance-attribute %s (running)", fix.InstanceID) _, err := fix.AWS.EC2.ModifyInstanceAttribute(&ec2.ModifyInstanceAttributeInput{ InstanceId: aws.String(fix.InstanceID), @@ -281,6 +298,7 @@ func phase8_NegativeErrorPaths(t *testing.T, fix *Fixture) { // 8r: RebootInstances on a non-existent instance id. t.Run("8r_RebootInstanceNotFound", func(t *testing.T) { + t.Parallel() harness.Step(t, "reboot-instances i-nonexistent") _, err := fix.AWS.EC2.RebootInstances(&ec2.RebootInstancesInput{ InstanceIds: []*string{aws.String("i-nonexistent")}, diff --git a/tests/e2e/single/snapshot_test.go b/tests/e2e/single/snapshot_test.go index 817f60be6..62b66a71c 100644 --- a/tests/e2e/single/snapshot_test.go +++ b/tests/e2e/single/snapshot_test.go @@ -54,7 +54,9 @@ func phase5c_SnapshotLifecycle(t *testing.T, fix *Fixture) { "progress", aws.StringValue(snap.Progress), ) - harness.WaitForSnapshotState(t, fix.AWS, fix.SnapshotID, "completed") + // Local QEMU finishes snapshot state transitions in <1s; tighten the + // default 2s polling to 500ms for the fast paths. + harness.WaitForSnapshotState(t, fix.AWS, fix.SnapshotID, "completed", harness.WithPoll(500*time.Millisecond)) harness.Step(t, "describe-snapshots %s", fix.SnapshotID) desc, err := fix.AWS.EC2.DescribeSnapshots(&ec2.DescribeSnapshotsInput{ @@ -83,7 +85,7 @@ func phase5c_SnapshotLifecycle(t *testing.T, fix *Fixture) { require.NotEqual(t, fix.SnapshotID, fix.CopySnapshotID, "copy snapshot ID should differ from original") harness.Detail(t, "copy_snapshot", fix.CopySnapshotID) - harness.WaitForSnapshotState(t, fix.AWS, fix.CopySnapshotID, "completed") + harness.WaitForSnapshotState(t, fix.AWS, fix.CopySnapshotID, "completed", harness.WithPoll(500*time.Millisecond)) harness.Step(t, "describe-snapshots %s,%s (both visible)", fix.SnapshotID, fix.CopySnapshotID) both, err := fix.AWS.EC2.DescribeSnapshots(&ec2.DescribeSnapshotsInput{ diff --git a/tests/e2e/single/volume_test.go b/tests/e2e/single/volume_test.go index dc39fed0e..dc08ca2b0 100644 --- a/tests/e2e/single/volume_test.go +++ b/tests/e2e/single/volume_test.go @@ -33,7 +33,10 @@ func phase5b_VolumeLifecycle(t *testing.T, fix *Fixture) { require.NotEmpty(t, fix.VolumeID, "create-volume returned empty VolumeId") harness.Detail(t, "volume", fix.VolumeID) - harness.WaitForVolumeState(t, fix.AWS, fix.VolumeID, "available") + // Local QEMU finishes volume state transitions in <1s; the default + // 2s polling adds avoidable wall-clock per phase. Tighten to 500ms + // for the fast paths (create→available, attach→in-use, detach→available). + harness.WaitForVolumeState(t, fix.AWS, fix.VolumeID, "available", harness.WithPoll(500*time.Millisecond)) const newSize int64 = 20 harness.Step(t, "modify-volume size=%d", newSize) @@ -70,7 +73,7 @@ func phase5b_VolumeLifecycle(t *testing.T, fix *Fixture) { }) require.NoError(t, err, "attach-volume") - harness.WaitForVolumeState(t, fix.AWS, fix.VolumeID, ec2.VolumeStateInUse) + harness.WaitForVolumeState(t, fix.AWS, fix.VolumeID, ec2.VolumeStateInUse, harness.WithPoll(500*time.Millisecond)) // Once the volume is in-use, the attachment record should be populated. descAttached, err := fix.AWS.EC2.DescribeVolumes(&ec2.DescribeVolumesInput{ @@ -90,7 +93,7 @@ func phase5b_VolumeLifecycle(t *testing.T, fix *Fixture) { }) require.NoError(t, err, "detach-volume") - harness.WaitForVolumeState(t, fix.AWS, fix.VolumeID, "available") + harness.WaitForVolumeState(t, fix.AWS, fix.VolumeID, "available", harness.WithPoll(500*time.Millisecond)) harness.Step(t, "delete-volume %s", fix.VolumeID) _, err = fix.AWS.EC2.DeleteVolume(&ec2.DeleteVolumeInput{