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.
This commit is contained in:
joshsiv-mulga
2026-05-18 22:44:13 +10:00
parent 19f11ffea9
commit afaf3fe8da
5 changed files with 75 additions and 33 deletions
+4
View File
@@ -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",
+43 -28
View File
@@ -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 {
+18
View File
@@ -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")},
+4 -2
View File
@@ -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{
+6 -3
View File
@@ -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{