Last active
May 20, 2021 00:00
-
-
Save ghale/227ec1dcb98aea35df4e146957e36527 to your computer and use it in GitHub Desktop.
Detecting overlapping outputs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
task checkForOverlappingOutputs { | |
doLast { | |
def rootNode = new TreeNode() | |
rootNode.name = '' | |
// Gather the outputs of all tasks | |
allprojects { | |
tasks.all { task -> | |
try { | |
task.outputs.files.each { file -> | |
def segments = file.absolutePath.split("/").drop(1) | |
rootNode.addChildren(segments).tasks.add(task.path) | |
} | |
} catch(Exception e) { | |
println "Failed to resolve task outputs of ${task.path}!" | |
} | |
} | |
} | |
// Walk the tree of outputs and look for overlaps | |
def overlaps = rootNode.getOverlaps() | |
// Remove any overlaps where the only overlaps are on the same task (i.e. the task has | |
// multiple outputs that overlap on each other) | |
def iterator = overlaps.iterator() | |
while (iterator.hasNext()) { | |
def overlap = iterator.next() | |
if (overlap.nodes.collect { it.tasks }.flatten().unique().size() < 2) { | |
iterator.remove() | |
} | |
} | |
def error = "" | |
overlaps.each { overlap -> | |
error += "\nOverlaps found for ${overlap.root}:\n" | |
overlap.nodes.each { node -> | |
error += "\t${node.toPath()} is an output of ${node.tasks.join(", ")}\n" | |
} | |
} | |
// Fail the task if we find any overlaps | |
if (overlaps.size() > 0) { | |
throw new RuntimeException("Found overlapping outputs:\n${error}") | |
} | |
} | |
} | |
class TreeNode { | |
String name | |
List<String> tasks = [] | |
TreeNode parent | |
Map<String, TreeNode> children = [:] | |
TreeNode addChildren(String[] segments) { | |
assert segments?.size() > 0 | |
def childName = segments[0] | |
def child | |
if (children[childName] != null) { | |
child = children[childName] | |
} else { | |
child = new TreeNode() | |
child.name = childName | |
child.parent = this | |
children[child.name] = child | |
} | |
if (segments.size() > 1) { | |
def grandChildren = segments.drop(1) | |
return child.addChildren(grandChildren) | |
} else { | |
return child | |
} | |
} | |
List<Overlap> getOverlaps() { | |
// If there are no children, we are at a leaf node, so it's only a question of whether there | |
// are overlaps on this node. | |
if (children.size() == 0) { | |
if (tasks.size() > 1) { | |
return [new Overlap(toPath(), [this])] | |
} else { | |
// this is a leaf node and it has no overlaps | |
return [] | |
} | |
} else { | |
// Otherwise, if this node has children, but no tasks, it is not the root of any overlap | |
if (tasks.isEmpty()) { | |
// Look for overlaps in children | |
return children.values().collect { it.getOverlaps() }.flatten() | |
} else { | |
// If it does have tasks and children, it is the root of an overlap | |
def overlap = new Overlap(toPath()) | |
overlap.nodes.addAll(getChildNodesWithTasks()) | |
return [overlap] | |
} | |
} | |
} | |
List<TreeNode> getChildNodesWithTasks() { | |
def nodes = [] | |
if (! tasks.empty) { | |
nodes << this | |
} | |
if (children.size() > 0) { | |
nodes.addAll(children.values().collect { it.getChildNodesWithTasks() }.flatten()) | |
} | |
return nodes | |
} | |
String toPath() { | |
if (parent) { | |
return "${parent.toPath()}/${name}" | |
} else { | |
return name | |
} | |
} | |
} | |
class Overlap { | |
String root | |
List<TreeNode> nodes | |
Overlap(String root, List<TreeNode> nodes = []) { | |
this.root = root | |
this.nodes = nodes | |
} | |
} | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment