|
| 1 | +// Copyright (c) Microsoft Corporation. |
| 2 | +// Licensed under the MIT License. |
| 3 | + |
| 4 | +package projectconfig_test |
| 5 | + |
| 6 | +import ( |
| 7 | + "reflect" |
| 8 | + "testing" |
| 9 | + |
| 10 | + "github.com/microsoft/azure-linux-dev-tools/internal/projectconfig" |
| 11 | + "github.com/stretchr/testify/assert" |
| 12 | +) |
| 13 | + |
| 14 | +// TestAllFingerprintedFieldsHaveDecision verifies that every field in every |
| 15 | +// fingerprinted struct has been consciously categorized as either included |
| 16 | +// (no fingerprint tag) or excluded (`fingerprint:"-"`). |
| 17 | +// |
| 18 | +// This test serves two purposes: |
| 19 | +// 1. It ensures that newly added fields default to **included** in the fingerprint |
| 20 | +// (the safe default — you get a false positive, never a false negative). |
| 21 | +// 2. It catches accidental removal of `fingerprint:"-"` tags from excluded fields, |
| 22 | +// since all exclusions are tracked in expectedExclusions. |
| 23 | +func TestAllFingerprintedFieldsHaveDecision(t *testing.T) { |
| 24 | + // All struct types whose fields participate in component fingerprinting. |
| 25 | + // When adding a new struct that feeds into the fingerprint, add it here. |
| 26 | + fingerprintedStructs := []reflect.Type{ |
| 27 | + reflect.TypeFor[projectconfig.ComponentConfig](), |
| 28 | + reflect.TypeFor[projectconfig.ComponentBuildConfig](), |
| 29 | + reflect.TypeFor[projectconfig.CheckConfig](), |
| 30 | + reflect.TypeFor[projectconfig.PackageConfig](), |
| 31 | + reflect.TypeFor[projectconfig.ComponentOverlay](), |
| 32 | + reflect.TypeFor[projectconfig.SpecSource](), |
| 33 | + reflect.TypeFor[projectconfig.DistroReference](), |
| 34 | + reflect.TypeFor[projectconfig.SourceFileReference](), |
| 35 | + } |
| 36 | + |
| 37 | + // Maps "StructName.FieldName" for every field that should carry a |
| 38 | + // `fingerprint:"-"` tag. Catches accidental tag removal. |
| 39 | + // |
| 40 | + // Each entry documents WHY the field is excluded from the fingerprint: |
| 41 | + expectedExclusions := map[string]bool{ |
| 42 | + // ComponentConfig.Name — metadata, already the map key in project config. |
| 43 | + "ComponentConfig.Name": true, |
| 44 | + // ComponentConfig.SourceConfigFile — internal bookkeeping reference, not a build input. |
| 45 | + "ComponentConfig.SourceConfigFile": true, |
| 46 | + |
| 47 | + // ComponentBuildConfig.Failure — CI policy (expected failure tracking), not a build input. |
| 48 | + "ComponentBuildConfig.Failure": true, |
| 49 | + // ComponentBuildConfig.Hints — scheduling hints (e.g. expensive), not a build input. |
| 50 | + "ComponentBuildConfig.Hints": true, |
| 51 | + |
| 52 | + // CheckConfig.SkipReason — human documentation for why check is skipped, not a build input. |
| 53 | + "CheckConfig.SkipReason": true, |
| 54 | + |
| 55 | + // PackageConfig.Publish — post-build routing (where to publish), not a build input. |
| 56 | + "PackageConfig.Publish": true, |
| 57 | + |
| 58 | + // ComponentOverlay.Description — human-readable documentation for the overlay. |
| 59 | + "ComponentOverlay.Description": true, |
| 60 | + |
| 61 | + // SourceFileReference.Component — back-reference to parent, not a build input. |
| 62 | + "SourceFileReference.Component": true, |
| 63 | + |
| 64 | + // DistroReference.Snapshot — snapshot timestamp is not a build input; the resolved |
| 65 | + // upstream commit hash (captured separately via SourceIdentity) is what matters. |
| 66 | + // Excluding this prevents a snapshot bump from marking all upstream components as changed. |
| 67 | + "DistroReference.Snapshot": true, |
| 68 | + |
| 69 | + // SourceFileReference.Origin — download location metadata (URI, type), not a build input. |
| 70 | + // The file content is already captured by Filename + Hash; changing a CDN URL should not |
| 71 | + // trigger a rebuild. |
| 72 | + "SourceFileReference.Origin": true, |
| 73 | + } |
| 74 | + |
| 75 | + // Collect all actual exclusions found via reflection, and flag invalid tag values. |
| 76 | + actualExclusions := make(map[string]bool) |
| 77 | + |
| 78 | + for _, st := range fingerprintedStructs { |
| 79 | + for i := range st.NumField() { |
| 80 | + field := st.Field(i) |
| 81 | + key := st.Name() + "." + field.Name |
| 82 | + |
| 83 | + tag := field.Tag.Get("fingerprint") |
| 84 | + |
| 85 | + switch tag { |
| 86 | + case "": |
| 87 | + // No tag — included by default (the safe default). |
| 88 | + case "-": |
| 89 | + actualExclusions[key] = true |
| 90 | + default: |
| 91 | + // hashstructure only recognises "" (include) and "-" (exclude). |
| 92 | + // Any other value is silently treated as included, which is |
| 93 | + // almost certainly a typo. |
| 94 | + assert.Failf(t, "invalid fingerprint tag", |
| 95 | + "field %q has unrecognised fingerprint tag value %q — "+ |
| 96 | + "only `fingerprint:\"-\"` (exclude) is valid; "+ |
| 97 | + "remove the tag to include the field", key, tag) |
| 98 | + } |
| 99 | + } |
| 100 | + } |
| 101 | + |
| 102 | + // Verify every expected exclusion is actually present. |
| 103 | + for key := range expectedExclusions { |
| 104 | + assert.Truef(t, actualExclusions[key], |
| 105 | + "expected field %q to have `fingerprint:\"-\"` tag, but it does not — "+ |
| 106 | + "was the tag accidentally removed?", key) |
| 107 | + } |
| 108 | + |
| 109 | + // Verify no unexpected exclusions exist. |
| 110 | + for key := range actualExclusions { |
| 111 | + assert.Truef(t, expectedExclusions[key], |
| 112 | + "field %q has `fingerprint:\"-\"` tag but is not in expectedExclusions — "+ |
| 113 | + "add it to expectedExclusions if the exclusion is intentional", key) |
| 114 | + } |
| 115 | +} |
0 commit comments