mirror of
https://github.com/mulgadc/spinifex.git
synced 2026-10-19 19:01:42 +00:00
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:
@@ -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",
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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")},
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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{
|
||||
|
||||
Reference in New Issue
Block a user