|
| 1 | +/* |
| 2 | + * Copyright 2026 Flamingock (https://www.flamingock.io) |
| 3 | + * |
| 4 | + * Licensed under the Apache License, Version 2.0 (the "License"); |
| 5 | + * you may not use this file except in compliance with the License. |
| 6 | + * You may obtain a copy of the License at |
| 7 | + * |
| 8 | + * http://www.apache.org/licenses/LICENSE-2.0 |
| 9 | + * |
| 10 | + * Unless required by applicable law or agreed to in writing, software |
| 11 | + * distributed under the License is distributed on an "AS IS" BASIS, |
| 12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 13 | + * See the License for the specific language governing permissions and |
| 14 | + * limitations under the License. |
| 15 | + */ |
| 16 | +package io.flamingock.internal.common.core.metadata; |
| 17 | + |
| 18 | +import io.flamingock.api.StageType; |
| 19 | +import io.flamingock.internal.common.core.preview.AbstractPreviewChange; |
| 20 | +import io.flamingock.internal.common.core.preview.CodePreviewChange; |
| 21 | +import io.flamingock.internal.common.core.preview.PreviewPipeline; |
| 22 | +import io.flamingock.internal.common.core.preview.PreviewStage; |
| 23 | +import io.flamingock.internal.common.core.preview.SystemPreviewStage; |
| 24 | +import org.junit.jupiter.api.DisplayName; |
| 25 | +import org.junit.jupiter.api.Test; |
| 26 | + |
| 27 | +import java.util.ArrayList; |
| 28 | +import java.util.Arrays; |
| 29 | +import java.util.Collections; |
| 30 | +import java.util.LinkedHashSet; |
| 31 | +import java.util.List; |
| 32 | +import java.util.Set; |
| 33 | +import java.util.stream.Collectors; |
| 34 | + |
| 35 | +import static org.junit.jupiter.api.Assertions.assertEquals; |
| 36 | +import static org.junit.jupiter.api.Assertions.assertNotNull; |
| 37 | + |
| 38 | +/** |
| 39 | + * Unit tests for {@code MetadataLoader.CompositePipelineBuilder}, focused on the |
| 40 | + * stage-collapse logic that was generalised to cover default stages (in addition to legacy |
| 41 | + * stages) so that mixed Java + Kotlin modules — where kapt and javac each produce their own |
| 42 | + * metadata provider with the same default stages — converge on a single default stage in the |
| 43 | + * composite instead of two side-by-side entries. |
| 44 | + */ |
| 45 | +class CompositePipelineBuilderTest { |
| 46 | + |
| 47 | + @Test |
| 48 | + @DisplayName("same-name default stages across modules collapse into one with merged change list") |
| 49 | + void defaultStagesWithSameNameAreCollapsed() { |
| 50 | + // Mirrors the kapt + javac scenario: two providers, same default stage, disjoint |
| 51 | + // changes (kapt contributed the Kotlin changes, javac contributed the Java ones). |
| 52 | + FlamingockMetadata moduleKapt = metadataWithDefaultStage( |
| 53 | + "main-stage", "com.example", changes("KotlinChangeA", "KotlinChangeB")); |
| 54 | + FlamingockMetadata moduleJavac = metadataWithDefaultStage( |
| 55 | + "main-stage", "com.example", changes("JavaChangeC")); |
| 56 | + |
| 57 | + PreviewPipeline composite = new MetadataLoader.CompositePipelineBuilder() |
| 58 | + .buildFrom(Arrays.asList(moduleKapt, moduleJavac)); |
| 59 | + |
| 60 | + List<PreviewStage> stages = new ArrayList<>(composite.getStages()); |
| 61 | + assertEquals(1, stages.size(), |
| 62 | + "Two providers contributing the same default stage must yield exactly one stage in the composite"); |
| 63 | + PreviewStage merged = stages.get(0); |
| 64 | + assertEquals("main-stage", merged.getName()); |
| 65 | + assertEquals(StageType.DEFAULT, merged.getType()); |
| 66 | + |
| 67 | + Set<String> changeIds = changeIds(merged); |
| 68 | + assertEquals(setOf("KotlinChangeA", "KotlinChangeB", "JavaChangeC"), changeIds, |
| 69 | + "Merged stage must contain the id-union of both providers' changes"); |
| 70 | + } |
| 71 | + |
| 72 | + @Test |
| 73 | + @DisplayName("same-name default stages with different sourcesPackage still collapse (first identity wins)") |
| 74 | + void defaultStagesWithMismatchedIdentityStillCollapse() { |
| 75 | + // The mismatch case: two modules declared a default stage with the same name but |
| 76 | + // different sourcesPackage. We still merge — the alternative (keep both stages) was |
| 77 | + // worse for any real use case — and first-seen identity wins. The behaviour change |
| 78 | + // also emits a WARN at runtime; we don't assert on logging here (no in-module log |
| 79 | + // capture infrastructure) and rely on code review for the WARN content. |
| 80 | + FlamingockMetadata moduleA = metadataWithDefaultStage( |
| 81 | + "shared-name", "com.example.a", changes("A1")); |
| 82 | + FlamingockMetadata moduleB = metadataWithDefaultStage( |
| 83 | + "shared-name", "com.example.b", changes("B1")); |
| 84 | + |
| 85 | + PreviewPipeline composite = new MetadataLoader.CompositePipelineBuilder() |
| 86 | + .buildFrom(Arrays.asList(moduleA, moduleB)); |
| 87 | + |
| 88 | + List<PreviewStage> stages = new ArrayList<>(composite.getStages()); |
| 89 | + assertEquals(1, stages.size(), |
| 90 | + "Mismatched-identity same-name stages must still collapse to a single stage"); |
| 91 | + PreviewStage merged = stages.get(0); |
| 92 | + assertEquals("com.example.a", merged.getSourcesPackage(), |
| 93 | + "First-seen sourcesPackage must win on identity-mismatch"); |
| 94 | + assertEquals(setOf("A1", "B1"), changeIds(merged), |
| 95 | + "Change sets must still be merged despite the identity mismatch"); |
| 96 | + } |
| 97 | + |
| 98 | + @Test |
| 99 | + @DisplayName("legacy stage collapse behaviour is preserved by the refactor") |
| 100 | + void legacyStagesStillCollapse() { |
| 101 | + // Regression guard: the legacy-stage collapse used to live in |
| 102 | + // mergeSameNameLegacyStages; after generalisation it routes through the same |
| 103 | + // mergeSameNameStages path. Verify same-name legacy stages still merge with id-dedup. |
| 104 | + FlamingockMetadata m1 = metadataWithStage(StageType.LEGACY, |
| 105 | + "flamingock-legacy-stage", null, changes("L1", "L2")); |
| 106 | + FlamingockMetadata m2 = metadataWithStage(StageType.LEGACY, |
| 107 | + "flamingock-legacy-stage", null, changes("L2", "L3")); |
| 108 | + |
| 109 | + PreviewPipeline composite = new MetadataLoader.CompositePipelineBuilder() |
| 110 | + .buildFrom(Arrays.asList(m1, m2)); |
| 111 | + |
| 112 | + List<PreviewStage> stages = new ArrayList<>(composite.getStages()); |
| 113 | + assertEquals(1, stages.size()); |
| 114 | + PreviewStage merged = stages.get(0); |
| 115 | + assertEquals(StageType.LEGACY, merged.getType()); |
| 116 | + assertEquals(setOf("L1", "L2", "L3"), changeIds(merged), |
| 117 | + "Duplicate legacy change ids across modules must be id-deduplicated"); |
| 118 | + } |
| 119 | + |
| 120 | + @Test |
| 121 | + @DisplayName("system stage id-dedup behaviour is preserved (separate code path, regression guard)") |
| 122 | + void systemStageDedupUnchanged() { |
| 123 | + // The system-stage merger (CompositePipelineBuilder#mergeSystem) is a different path |
| 124 | + // from collapseStagesByName and is unchanged by this refactor. Sanity-check it. |
| 125 | + SystemPreviewStage sysA = new SystemPreviewStage("system-stage", "desc", null, null, |
| 126 | + new ArrayList<>(changes("S1", "S2"))); |
| 127 | + SystemPreviewStage sysB = new SystemPreviewStage("system-stage", "desc", null, null, |
| 128 | + new ArrayList<>(changes("S2", "S3"))); |
| 129 | + FlamingockMetadata m1 = new FlamingockMetadata(); |
| 130 | + m1.setPipeline(new PreviewPipeline(sysA, Collections.emptyList())); |
| 131 | + FlamingockMetadata m2 = new FlamingockMetadata(); |
| 132 | + m2.setPipeline(new PreviewPipeline(sysB, Collections.emptyList())); |
| 133 | + |
| 134 | + PreviewPipeline composite = new MetadataLoader.CompositePipelineBuilder() |
| 135 | + .buildFrom(Arrays.asList(m1, m2)); |
| 136 | + |
| 137 | + PreviewStage system = composite.getSystemStage(); |
| 138 | + assertNotNull(system, "Composite must carry a system stage when contributors had one"); |
| 139 | + assertEquals(setOf("S1", "S2", "S3"), changeIds(system), |
| 140 | + "Duplicate system-stage change ids across modules must be id-deduplicated"); |
| 141 | + } |
| 142 | + |
| 143 | + @Test |
| 144 | + @DisplayName("default stages with distinct names remain distinct (sanity: not over-collapsing)") |
| 145 | + void distinctNameDefaultStagesAreKeptSeparate() { |
| 146 | + // Guard against the lazy implementation that would collapse everything into one |
| 147 | + // group. Distinct names must stay distinct. |
| 148 | + FlamingockMetadata m1 = metadataWithDefaultStage("alpha", "com.alpha", changes("a1")); |
| 149 | + FlamingockMetadata m2 = metadataWithDefaultStage("beta", "com.beta", changes("b1")); |
| 150 | + |
| 151 | + PreviewPipeline composite = new MetadataLoader.CompositePipelineBuilder() |
| 152 | + .buildFrom(Arrays.asList(m1, m2)); |
| 153 | + |
| 154 | + List<PreviewStage> stages = new ArrayList<>(composite.getStages()); |
| 155 | + assertEquals(2, stages.size(), "Distinct-name stages must remain separate"); |
| 156 | + Set<String> names = stages.stream().map(PreviewStage::getName).collect(Collectors.toSet()); |
| 157 | + assertEquals(setOf("alpha", "beta"), names); |
| 158 | + } |
| 159 | + |
| 160 | + // ---------------------------------------------------------------------- |
| 161 | + // Fixture helpers |
| 162 | + // ---------------------------------------------------------------------- |
| 163 | + |
| 164 | + private static FlamingockMetadata metadataWithDefaultStage(String name, |
| 165 | + String sourcesPackage, |
| 166 | + List<AbstractPreviewChange> changes) { |
| 167 | + return metadataWithStage(StageType.DEFAULT, name, sourcesPackage, changes); |
| 168 | + } |
| 169 | + |
| 170 | + private static FlamingockMetadata metadataWithStage(StageType type, |
| 171 | + String name, |
| 172 | + String sourcesPackage, |
| 173 | + List<AbstractPreviewChange> changes) { |
| 174 | + PreviewStage stage = new PreviewStage(name, type, null, sourcesPackage, null, changes); |
| 175 | + FlamingockMetadata md = new FlamingockMetadata(); |
| 176 | + md.setPipeline(new PreviewPipeline(Collections.singletonList(stage))); |
| 177 | + return md; |
| 178 | + } |
| 179 | + |
| 180 | + private static List<AbstractPreviewChange> changes(String... ids) { |
| 181 | + List<AbstractPreviewChange> result = new ArrayList<>(ids.length); |
| 182 | + for (String id : ids) { |
| 183 | + CodePreviewChange change = new CodePreviewChange(); |
| 184 | + change.setId(id); |
| 185 | + result.add(change); |
| 186 | + } |
| 187 | + return result; |
| 188 | + } |
| 189 | + |
| 190 | + private static Set<String> changeIds(PreviewStage stage) { |
| 191 | + if (stage.getChanges() == null) return Collections.emptySet(); |
| 192 | + Set<String> ids = new LinkedHashSet<>(); |
| 193 | + for (AbstractPreviewChange c : stage.getChanges()) { |
| 194 | + ids.add(c.getId()); |
| 195 | + } |
| 196 | + return ids; |
| 197 | + } |
| 198 | + |
| 199 | + private static Set<String> setOf(String... values) { |
| 200 | + return new LinkedHashSet<>(Arrays.asList(values)); |
| 201 | + } |
| 202 | +} |
0 commit comments