diff --git a/core/src/main/java/jenkins/scm/RunWithSCM.java b/core/src/main/java/jenkins/scm/RunWithSCM.java index 5c784b175f36..f6a04aa5bd1f 100644 --- a/core/src/main/java/jenkins/scm/RunWithSCM.java +++ b/core/src/main/java/jenkins/scm/RunWithSCM.java @@ -34,6 +34,7 @@ import hudson.scm.SCM; import hudson.util.AdaptedIterator; import java.util.AbstractSet; +import java.util.Collections; import java.util.HashSet; import java.util.Iterator; import java.util.List; @@ -93,8 +94,12 @@ public interface RunWithSCM, return calculateCulprits(); } + Set ids = getCulpritIds(); + return new AbstractSet<>() { - private Set culpritIds = Set.copyOf(getCulpritIds()); + + private final Set culpritIds = (ids == null) + ? Collections.emptySet() : Collections.unmodifiableSet(new HashSet<>(ids)); @Override public Iterator iterator() { diff --git a/core/src/test/java/jenkins/scm/RunWithSCMTest.java b/core/src/test/java/jenkins/scm/RunWithSCMTest.java new file mode 100644 index 000000000000..d44bd2f835e5 --- /dev/null +++ b/core/src/test/java/jenkins/scm/RunWithSCMTest.java @@ -0,0 +1,92 @@ +package jenkins.scm; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import hudson.model.User; +import hudson.scm.ChangeLogSet; +import java.util.AbstractSet; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Set; +import org.jspecify.annotations.NonNull; +import org.junit.jupiter.api.Test; + +class RunWithSCMTest { + + /** + * Regression test for corrupted UnmodifiableCollection + * Set.copyOf() throws NPE + * This was a regression introduced when migrating from Collections.unmodifiableSet to Set.copyOf. + */ + @Test + void getCulprits_shouldHandleCorruptedCollectionThatThrowsNPE() { + RunWithSCM run = new RunWithSCM() { + @Override + public boolean shouldCalculateCulprits() { + return false; + } + + @Override + public @NonNull List> getChangeSets() { + return List.of(); + } + + @Override + public Set getCulpritIds() { + // Simulates corrupted UnmodifiableCollection + return new AbstractSet<>() { + @Override + public Iterator iterator() { + return Collections.emptyIterator(); + } + + @Override + public int size() { + return 0; + } + + @Override + public boolean isEmpty() { + throw new NullPointerException("c is null"); + } + }; + } + }; + + // Should not throw NPE, should return empty set + Set culprits = run.getCulprits(); + + assertNotNull(culprits); + assertEquals(0, culprits.size()); + } + + /** + * Tests that null return from getCulpritIds() is handled gracefully. + */ + @Test + void getCulprits_shouldHandleNullCulpritIds() { + RunWithSCM run = new RunWithSCM() { + @Override + public boolean shouldCalculateCulprits() { + return false; + } + + @Override + public @NonNull List> getChangeSets() { + return List.of(); + } + + @Override + public Set getCulpritIds() { + return null; + } + }; + + Set culprits = run.getCulprits(); + + assertNotNull(culprits); + assertEquals(0, culprits.size()); + } +}