-
Notifications
You must be signed in to change notification settings - Fork 7
Variability-aware patching of software product-line variants #179
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: thesis_pm
Are you sure you want to change the base?
Changes from all commits
1b54094
692d6c6
c35bebc
91f3ffa
24cf2bf
1b59508
28cd47b
c9f05da
8ac9a86
58acae5
0437b28
a724fb9
836d6ab
dcf2d88
bb766ae
6fbdcb5
0994bae
a3a02fb
5fdc441
02aa6ba
7cba4bd
b6e835a
dbe6ba5
c67ea43
8a33fb9
39e0483
db7a551
cde3999
8e81fa7
cb25dce
5e58efb
e6a5ad6
170206d
86d4c22
33efa25
3d3fd1c
f31cfbf
7e94413
b48ac2d
dddff0f
9da7092
a7c6672
580a777
aa4c8f0
b1c149e
4bd5546
77b2d6a
ff05f1d
49018ae
38ac7fe
76eaa2a
01c2fe4
1d3e9e0
d1237be
10693dd
831344f
e0adf36
c1287d5
a798ab8
bea648d
386b370
80561b0
dd34a35
0c0cbe2
f2158a4
5dc1705
c1a2322
0a711fc
1df8e22
3ffeb1e
93a0841
173c77d
b5110e8
4f3a895
ec7816d
53ec6b5
6175510
0793eca
259199a
7b0e8bd
6e2a888
3a2a14c
2e5d5db
d9cf4c7
9f5fba6
c2f2f9b
beef266
462f36c
1678346
6ab67a1
78f3885
35a414e
ff61a86
5a95a5b
f45fffa
bcbc652
8e9c386
7b58ccd
773eda2
4583e7b
e839aae
a6ddc0c
5c1cfbf
00118cd
53a697b
116fa18
2ad7eac
5dc4043
65d681b
2f145bd
762d476
79d2382
174b291
371bd40
783ec2d
3bac321
ae4d808
ffe3056
4d75b41
ec8f0c2
755c2bd
eacb6ab
51d5bde
6555f03
9665928
996e53d
5720cc5
531b30c
69f46fd
eea15e0
8eaebed
7f200e8
5b5a4cb
f65479f
7c4366a
e556df0
a138e4f
bcef3b7
b55b781
88d6f38
c0f3a4f
13561e7
6da8855
1a3b70f
a3c4098
71340ca
df60cf1
c001d16
1f09fbc
577d6e1
8175f2f
d6f3684
ea50301
a7c5bbb
e4975db
7859361
51b97ae
08a4587
8e79d24
d2247c4
9bc7ae0
97433b8
2878951
a7b2208
784e24c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| package org.variantsync.diffdetective.experiments.thesis_pm; | ||
|
|
||
| import org.variantsync.diffdetective.variation.DiffLinesLabel; | ||
| import org.variantsync.diffdetective.variation.Label; | ||
| import org.variantsync.diffdetective.variation.diff.Time; | ||
| import org.variantsync.diffdetective.variation.diff.VariationDiff; | ||
| import org.variantsync.diffdetective.variation.diff.view.DiffView; | ||
| import org.variantsync.diffdetective.variation.tree.VariationTree; | ||
| import org.variantsync.diffdetective.variation.tree.view.TreeView; | ||
| import org.variantsync.diffdetective.variation.tree.view.relevance.Configure; | ||
| import org.variantsync.diffdetective.variation.tree.view.relevance.ConfigureWithFullConfig; | ||
| import org.variantsync.diffdetective.variation.tree.view.relevance.Unchanged; | ||
|
|
||
| public class PatchScenario<L extends Label> { | ||
| public VariationDiff<L> sourcePatch; | ||
| public VariationTree<L> targetVariantBefore; | ||
| public VariationDiff<L> patchGroundTruth; | ||
| public VariationTree<L> patchedVariantGroundTruth; | ||
| public ConfigureWithFullConfig sourceVariantConfig; | ||
| public ConfigureWithFullConfig targetVariantConfig; | ||
| public VariationTree<DiffLinesLabel> sourceVariantAfterRedToCrossVarFeatures; | ||
| public Unchanged unchangedAfter; | ||
| public VariationTree<DiffLinesLabel> targetVariantBeforeRedToUnchanged; | ||
|
|
||
| public PatchScenario(VariationDiff<L> sourcePatch, | ||
| VariationTree<L> targetVariantBefore, VariationDiff<L> patchGroundTruth, | ||
| VariationTree<L> patchedVariantGroundTruth, ConfigureWithFullConfig sourceVariantConfig, ConfigureWithFullConfig targetVariantConfig) { | ||
| this.sourcePatch = sourcePatch; | ||
| this.targetVariantBefore = targetVariantBefore; | ||
| this.patchGroundTruth = patchGroundTruth; | ||
| this.patchedVariantGroundTruth = patchedVariantGroundTruth; | ||
| this.sourceVariantConfig = sourceVariantConfig; | ||
| this.targetVariantConfig = targetVariantConfig; | ||
| this.sourceVariantAfterRedToCrossVarFeatures = (VariationTree<DiffLinesLabel>) TreeView.tree(sourcePatch.project(Time.AFTER), this.targetVariantConfig); | ||
| VariationDiff<DiffLinesLabel> sourcePatchConfiguredToCrossVarFeatures = (VariationDiff<DiffLinesLabel>) DiffView.optimized(sourcePatch, targetVariantConfig); | ||
| this.unchangedAfter = new Unchanged((VariationDiff<DiffLinesLabel>) sourcePatchConfiguredToCrossVarFeatures, Time.AFTER); | ||
| Unchanged unchangedBefore = new Unchanged((VariationDiff<DiffLinesLabel>) sourcePatchConfiguredToCrossVarFeatures, Time.BEFORE); | ||
| this.targetVariantBeforeRedToUnchanged = (VariationTree<DiffLinesLabel>) TreeView.tree(this.targetVariantBefore, unchangedBefore); | ||
| } | ||
| } |
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,119 @@ | ||
| package org.variantsync.diffdetective.experiments.thesis_pm; | ||
|
|
||
| import java.io.IOException; | ||
| import java.nio.file.Path; | ||
| import java.util.HashSet; | ||
| import java.util.Iterator; | ||
| import java.util.Set; | ||
|
|
||
| import org.eclipse.jgit.diff.DiffAlgorithm; | ||
| import org.eclipse.jgit.diff.DiffAlgorithm.SupportedAlgorithm; | ||
| import org.variantsync.diffdetective.diff.result.DiffParseException; | ||
| import org.variantsync.diffdetective.variation.DiffLinesLabel; | ||
| import org.variantsync.diffdetective.variation.Label; | ||
| import org.variantsync.diffdetective.variation.diff.DiffNode; | ||
| import org.variantsync.diffdetective.variation.diff.VariationDiff; | ||
| import org.variantsync.diffdetective.variation.diff.parse.VariationDiffParseOptions; | ||
| import org.variantsync.diffdetective.variation.tree.VariationTree; | ||
| import org.variantsync.diffdetective.variation.tree.view.TreeView; | ||
| import org.variantsync.diffdetective.variation.tree.view.relevance.ConfigureWithFullConfig; | ||
| import org.variantsync.diffdetective.variation.tree.view.relevance.Unchanged; | ||
| import org.variantsync.functjonal.Pair; | ||
|
|
||
| public class Utils { | ||
|
|
||
| public static boolean comparePatchedVariantWithExpectedResult(VariationTree<DiffLinesLabel> patchedVariant, | ||
| VariationTree<DiffLinesLabel> expectedResult) { | ||
| return Utils.isSameAs(patchedVariant.toCompletelyUnchangedVariationDiff(), | ||
| expectedResult.toCompletelyUnchangedVariationDiff()); | ||
| } | ||
|
|
||
| public static Pair<Boolean, Boolean> arePatchedVariantsEquivalent( | ||
| VariationTree<DiffLinesLabel> patchedTargetVariant, | ||
| VariationTree<DiffLinesLabel> sourceVariantAfterRedToCrossVarFeatures, | ||
| VariationTree<DiffLinesLabel> targetVariantBeforeRedToUnchanged, ConfigureWithFullConfig configSourceVariant, | ||
| Unchanged unchangedAfter) { | ||
|
|
||
| VariationTree<DiffLinesLabel> targetVariantAfterRedToCrossVarFeatures = TreeView.tree(patchedTargetVariant, | ||
| configSourceVariant); | ||
|
|
||
| VariationTree<DiffLinesLabel> patchedTargetVariantRedToUnchanged = TreeView.tree(patchedTargetVariant, | ||
| unchangedAfter); | ||
|
|
||
| // GameEngine.showAndAwaitAll(Show.tree(patchedTargetVariant, "patched target variant"), | ||
| // Show.tree(patchedTargetVariantRedToUnchanged, "patched target variant red. to unchanged"), | ||
| // Show.tree(sourceVariantAfterRedToCrossVarFeatures, | ||
| // "patched source variant red. to cross variant features"), | ||
| // Show.tree(targetVariantAfterRedToCrossVarFeatures, | ||
| // "patched target variant red. to cross variant features"), | ||
| // Show.tree(targetVariantBeforeRedToUnchanged, "target variant before red. to unchanged")); | ||
|
|
||
| return new Pair<Boolean, Boolean>( | ||
| sourceVariantAfterRedToCrossVarFeatures.unparse().equals(targetVariantAfterRedToCrossVarFeatures.unparse()), | ||
| patchedTargetVariantRedToUnchanged.unparse().equals(targetVariantBeforeRedToUnchanged.unparse())); | ||
|
|
||
| } | ||
|
|
||
| public static VariationDiff<DiffLinesLabel> parseVariationDiffFromFiles(String file1, String file2) | ||
| throws IOException, DiffParseException { | ||
| Path examplesDir = Path.of("data", "examples"); | ||
| return VariationDiff.fromFiles(examplesDir.resolve(file1), examplesDir.resolve(file2), | ||
| DiffAlgorithm.SupportedAlgorithm.MYERS, VariationDiffParseOptions.Default); | ||
| } | ||
|
|
||
| public static VariationTree<DiffLinesLabel> parseVariationTreeFromFile(String file) { | ||
| Path examplesDir = Path.of("data", "examples"); | ||
| Path path = examplesDir.resolve(file); | ||
| try { | ||
| VariationTree<DiffLinesLabel> tree = VariationTree.fromFile(path, VariationDiffParseOptions.Default); | ||
| return tree; | ||
| } catch (IOException e) { | ||
| e.printStackTrace(); | ||
| } catch (DiffParseException e) { | ||
| e.printStackTrace(); | ||
| } | ||
| return null; | ||
| } | ||
|
|
||
| public static VariationDiff<DiffLinesLabel> parseVariationDiffFromFile(String file) | ||
| throws IOException, DiffParseException { | ||
| Path examplesDir = Path.of("data", "examples"); | ||
| return VariationDiff.fromFile(examplesDir.resolve(file), VariationDiffParseOptions.Default); | ||
| } | ||
|
|
||
| public static <L extends Label> boolean isSameAs(VariationDiff<L> diff1, VariationDiff<L> diff2) { | ||
| return Utils.isSameAs(diff1.getRoot(), diff2.getRoot()); | ||
| } | ||
|
|
||
| public static <L extends Label> boolean isSameAs(DiffNode<L> a, DiffNode<L> b) { | ||
| return Utils.isSameAs(a, b, new HashSet<>()); | ||
| } | ||
|
|
||
| public static <L extends Label> boolean isSameAs(DiffNode<L> a, DiffNode<L> b, Set<DiffNode<L>> visited) { | ||
| if (!visited.add(a)) { | ||
| return true; | ||
| } | ||
|
|
||
| if (!(a.getNodeType().equals(b.getNodeType()) && hasSameLabel(a.getLabel(), b.getLabel()) | ||
| && (a.getFormula() == null ? b.getFormula() == null : a.getFormula().equals(b.getFormula())))) { | ||
| return false; | ||
| } | ||
|
|
||
| Iterator<DiffNode<L>> aIt = a.getAllChildren().iterator(); | ||
| Iterator<DiffNode<L>> bIt = b.getAllChildren().iterator(); | ||
| while (aIt.hasNext() && bIt.hasNext()) { | ||
| if (!isSameAs(aIt.next(), bIt.next(), visited)) { | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| return aIt.hasNext() == bIt.hasNext(); | ||
| } | ||
|
|
||
| public static <L extends Label> boolean hasSameLabel(L a, L b) { | ||
| String labelA = a.toString().replaceAll(" ", ""); | ||
| String labelB = b.toString().replaceAll(" ", ""); | ||
| return labelA.equals(labelB); | ||
| } | ||
|
|
||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,55 @@ | ||
| package org.variantsync.diffdetective.shell; | ||
|
|
||
| import java.util.List; | ||
|
|
||
| /** Single executable command with arguments. */ | ||
| public class DiffCommand extends ShellCommand { | ||
| private final String[] parts; | ||
| private boolean filesDifferent; | ||
|
|
||
| /** | ||
| * Constructs a single command. | ||
| * The first argument has to be a path to an executable which will be given all of the | ||
| * remaining arguments as parameters on execution. | ||
| * | ||
| * @param cmd executable path and arguments for the executable | ||
| */ | ||
| public DiffCommand(final String... cmd) { | ||
| parts = cmd; | ||
| } | ||
|
|
||
| @Override | ||
| public String[] parts() { | ||
| return parts; | ||
| } | ||
|
|
||
| /** | ||
| * Interpret the result/exit code returned from a shell command. | ||
| * An {@code ShellException} is thrown if the result code is an error. | ||
| * | ||
| * @param resultCode the code that is to be parsed | ||
| * @param output the output of the shell command | ||
| * @return the output of the shell command | ||
| * @throws ShellException if {@code resultCode} is an error | ||
| */ | ||
| @Override | ||
| public List<String> interpretResult(int resultCode, List<String> output) throws ShellException { | ||
| // inputs are the same | ||
| if (resultCode == 0) { | ||
| filesDifferent = false; | ||
| return output; | ||
| } | ||
| // if inputs are different | ||
| if (resultCode == 1) { | ||
| filesDifferent = true; | ||
| return output; | ||
| } | ||
| // everything else: "serious trouble": exit code 2 | ||
| throw new ShellException(output); | ||
|
|
||
| } | ||
|
|
||
| public boolean areFilesDifferent() { | ||
| return filesDifferent; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,55 @@ | ||
| package org.variantsync.diffdetective.shell; | ||
|
|
||
| import java.util.List; | ||
|
|
||
| /** Single executable command with arguments. */ | ||
| public class GnuPatchCommand extends ShellCommand { | ||
| private final String[] parts; | ||
| private boolean patchingSuccessful; | ||
|
|
||
| /** | ||
| * Constructs a single command. | ||
| * The first argument has to be a path to an executable which will be given all of the | ||
| * remaining arguments as parameters on execution. | ||
| * | ||
| * @param cmd executable path and arguments for the executable | ||
| */ | ||
| public GnuPatchCommand(final String... cmd) { | ||
| parts = cmd; | ||
| } | ||
|
|
||
| @Override | ||
| public String[] parts() { | ||
| return parts; | ||
| } | ||
|
|
||
| /** | ||
| * Interpret the result/exit code returned from a shell command. | ||
| * An {@code ShellException} is thrown if the result code is an error. | ||
| * | ||
| * @param resultCode the code that is to be parsed | ||
| * @param output the output of the shell command | ||
| * @return the output of the shell command | ||
| * @throws ShellException if {@code resultCode} is an error | ||
| */ | ||
| @Override | ||
| public List<String> interpretResult(int resultCode, List<String> output) throws ShellException { | ||
| // patching was successful | ||
| if (resultCode == 0) { | ||
| patchingSuccessful = true; | ||
| return null; | ||
| } | ||
| // some hunks cannot be applied or merge conflicts | ||
| if (resultCode == 1) { | ||
| patchingSuccessful = false; | ||
| return null; | ||
| } | ||
| // everything else: "serious trouble": exit code 2 | ||
| throw new ShellException(output); | ||
|
|
||
| } | ||
|
|
||
| public boolean isPatchingSuccessful() { | ||
| return patchingSuccessful; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,55 @@ | ||
| package org.variantsync.diffdetective.shell; | ||
|
|
||
| import java.util.List; | ||
|
|
||
| /** Single executable command with arguments. */ | ||
| public class MPatchCommand extends ShellCommand { | ||
| private final String[] parts; | ||
| private boolean patchingSuccessful; | ||
|
|
||
| /** | ||
| * Constructs a single command. | ||
| * The first argument has to be a path to an executable which will be given all of the | ||
| * remaining arguments as parameters on execution. | ||
| * | ||
| * @param cmd executable path and arguments for the executable | ||
| */ | ||
| public MPatchCommand(final String... cmd) { | ||
| parts = cmd; | ||
| } | ||
|
|
||
| @Override | ||
| public String[] parts() { | ||
| return parts; | ||
| } | ||
|
|
||
| /** | ||
| * Interpret the result/exit code returned from a shell command. | ||
| * An {@code ShellException} is thrown if the result code is an error. | ||
| * | ||
| * @param resultCode the code that is to be parsed | ||
| * @param output the output of the shell command | ||
| * @return the output of the shell command | ||
| * @throws ShellException if {@code resultCode} is an error | ||
| */ | ||
| @Override | ||
| public List<String> interpretResult(int resultCode, List<String> output) throws ShellException { | ||
| // patching was successful ??? | ||
| if (resultCode == 0) { | ||
| patchingSuccessful = true; | ||
| return null; | ||
| } | ||
| // some hunks cannot be applied or merge conflicts ??? | ||
| if (resultCode == 1) { | ||
| patchingSuccessful = false; | ||
| return null; | ||
| } | ||
| // everything else: "serious trouble": exit code 2 ??? | ||
| throw new ShellException(output); | ||
|
|
||
| } | ||
|
|
||
| public boolean isPatchingSuccessful() { | ||
| return patchingSuccessful; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -378,16 +378,16 @@ public int count(final Predicate<DiffNode<L>> nodesToCount) { | |
| * This method is deterministic: It will return the feature names always in the same order, assuming the variation diff is not changed inbetween. | ||
| * @return A set of every occuring feature name. | ||
| */ | ||
| public LinkedHashSet<String> computeAllFeatureNames() { | ||
| LinkedHashSet<String> features = new LinkedHashSet<>(); | ||
| public LinkedHashSet<Object> computeAllFeatureNames() { | ||
| LinkedHashSet<Object> features = new LinkedHashSet<>(); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why was it necessary to turn these strings into objects, only to recover that lost information by casting a few lines later?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I remember that we had problems with other representations of true and false than the strings "True"/"False", e. g., there were also 0 and 1 in the examples. But the change seems a bit weird. Probably, we can drop this change. |
||
| forAll(node -> { | ||
| if (node.isConditionalAnnotation()) { | ||
| features.addAll(node.getFormula().getUniqueContainedFeatures()); | ||
| } | ||
| }); | ||
| // Since FeatureIDE falsely reports constants "True" and "False" as feature names, we have to remove them from the resulting set. | ||
| features.removeIf(FixTrueFalse::isTrueLiteral); | ||
| features.removeIf(FixTrueFalse::isFalseLiteral); | ||
| features.removeIf(f -> FixTrueFalse.isTrueLiteral((String) f)); | ||
| features.removeIf(f -> FixTrueFalse.isFalseLiteral((String) f)); | ||
| return features; | ||
| } | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess this change is sensible. I do not remember why we chose CRLF here in the first place.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As the comment states, we introduced
LINEBREAKinstead of usingSystem.lineSeparatorto ensure that the line endings of our outputs are independent of the operating system. In particular, if I remember correctly, there was an issue with some dependency (something line graph related according to the Git history) that caused issues on Windows. As windows applications typically take issue with parsing text files without carriage returns while Linux applications just treat the carriage return as data, we use '\r\n' for all outputs.In summary, the biggest potential issue is probably Windows compatibility. As the output of DiffDetective will not match the expectations in that environment. However, I think we don't use this constant very consistently anyways.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for the detailed analysis. If I remember correctly, some tests that read/write files failed on Windows but not Linux. 🤔
Maybe it is safest to just leave LINEBREAK as is, and maybe add a second constant
LINEBREAK_LFfor code parts of @piameier.