Uh oh!
There was an error while loading. Please reload this page.
- 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?
Uh oh!
There was an error while loading. Please reload this page.
Changes from all commits
1b54094692d6c6c35bebc91f3ffa24cf2bf1b5950828cd47bc9f05da8ac9a8658acae50437b28a724fb9836d6abdcf2d88bb766ae6fbdcb50994baea3a02fb5fdc44102aa6ba7cba4bdb6e835adbe6ba5c67ea438a33fb939e0483db7a551cde39998e81fa7cb25dce5e58efbe6a5ad6170206d86d4c2233efa253d3fd1cf31cfbf7e94413b48ac2ddddff0f9da7092a7c6672580a777aa4c8f0b1c149e4bd554677b2d6aff05f1d49018ae38ac7fe76eaa2a01c2fe41d3e9e0d1237be10693dd831344fe0adf36c1287d5a798ab8bea648d386b37080561b0dd34a350c0cbe2f2158a45dc1705c1a23220a711fc1df8e223ffeb1e93a0841173c77db5110e84f3a895ec7816d53ec6b561755100793eca259199a7b0e8bd6e2a8883a2a14c2e5d5dbd9cf4c79f5fba6c2f2f9bbeef266462f36c16783466ab67a178f388535a414eff61a865a95a5bf45fffabcbc6528e9c3867b58ccd773eda24583e7be839aaea6ddc0c5c1cfbf00118cd53a697b116fa182ad7eac5dc404365d681b2f145bd762d47679d2382174b291371bd40783ec2d3bac321ae4d808ffe30564d75b41ec8f0c2755c2bdeacb6ab51d5bde6555f039665928996e53d5720cc5531b30c69f46fdeea15e08eaebed7f200e85b5a4cbf65479f7c4366ae556df0a138e4fbcef3b7b55b78188d6f38c0f3a4f13561e76da88551a3b70fa3c409871340cadf60cf1c001d161f09fbc577d6e18175f2fd6f3684ea50301a7c5bbbe4975db785936151b97ae08a45878e79d24d2247c49bc7ae097433b82878951a7b2208784e24cFile filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Uh oh!
There was an error while loading. Please reload this page.
| 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.
Uh oh!
There was an error while loading. Please reload this page.
| 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? ContributorAuthor 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. 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. Ok. To fix the issues with names for true and false, there is also | ||
| 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; | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
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.