def test_ids_assigned_in_change(self):
        manager = Manager(
            excel_path=[
                (DATA_DIRECTORY / "Composition Example 2 Model Baseline.xlsx"),
                (DATA_DIRECTORY / "Composition Example 2 Model Changed.xlsx"),
            ],
            json_path=[(PATTERNS / "Composition.json")],
        )
        eval_base = manager.evaluators[0]
        eval_change = manager.evaluators[1]

        eval_base.rename_df_columns()
        eval_base.add_missing_columns()
        eval_base.to_property_di_graph()

        eval_change.rename_df_columns()
        eval_change.add_missing_columns()
        eval_change.to_property_di_graph()

        self.assertTrue(
            set(eval_base.translator.uml_id.keys()).issubset(
                set(eval_change.translator.uml_id.keys())))
        for key in eval_base.translator.uml_id.keys():
            if key != "count":
                assert (eval_base.translator.uml_id[key] ==
                        eval_change.translator.uml_id[key])
 def test_create_evaluators(self):
     manager = Manager(
         excel_path=[
             DATA_DIRECTORY / "Composition Example.xlsx" for i in range(2)
         ],
         json_path=[PATTERNS / "Composition.json"],
     )
     # weak test: create_evaluators() run during init
     self.assertEqual(2, len(manager.evaluators))
     for eval in manager.evaluators:
         self.assertIsInstance(eval, Evaluator)
    def test_get_json_data(self):
        manager = Manager(
            excel_path=[
                DATA_DIRECTORY / "Composition Example.xlsx" for i in range(2)
            ],
            json_path=[PATTERNS / "Composition.json"],
        )
        expected_keys = [
            "Columns to Navigation Map",
            "Pattern Graph Edges",
            "Root Node",
            "Vertex MetaTypes",
            "Vertex Settings",
            "Vertex Stereotypes",
        ]

        assert expected_keys == list(manager.json_data[0].keys())
Exemple #4
0
def compare_md_model(inputs, input_patterns="", output_path=""):
    """
    Produces difference files (JSON and Excel) for the original file to
    each change file provided.

    Parameters
    ----------
    inputs : list of strs
        List of one or more file paths parsed from the command line.
        This can be a path to one or more Excel files or a path to a
        directory of Excel files.

    input_patterns : list of str
        List of paths to pattern file provided by the user.

    output_path : str
        String of the desired location for the output. This is optional
        and if omitted then the output files will be placed in the same
        directory as the input files.

    Returns
    -------
    output_json : JSON file
        Generates a JSON file as output that the Player Piano digests to
        Generates a JSON file as output that the Player Piano digests to
        update the original Model.
    output_excel : Excel file
        Generates an Excel file that lists the confident changes (ones
        made by the JSON) and the unstable pairs so the user can make the
        determination on those changes on their own.
    """
    provided_paths = inputs
    wkbk_paths = []
    here = Path(os.getcwd())

    for counter, path in enumerate(provided_paths):
        p = Path(path)
        if not p.is_absolute():
            p = here / p
        if p.is_dir():
            p = list(p.glob("*.xlsx"))
            for path in p:
                if counter != 0 and path.name == p[0].name:
                    p.remove(path)
        else:
            p = [p]

        wkbk_paths.extend(p)

    if output_path:
        output_path = Path(output_path)
        if not output_path.is_absolute():
            if output_path.parts[-1] == here.parts[-1]:
                output_path = here
            else:
                output_path = here / output_path
        if not output_path.is_dir():
            raise RuntimeError("Please provide an output directory")
        outpath = output_path
    else:
        outpath = wkbk_paths[0].parent

    for wkbk in wkbk_paths:
        if wkbk.parts[-1].split(".")[-1] != "xlsx":
            msg = ("\n" + "This program only supports Excel Files." +
                   ' "{0}" was skipped, not an Excel File').format(
                       wkbk.parts[-1])
            warnings.warn(msg)
            continue

    json_patterns = {
        pattern_path.name.split(".")[0].lower(): pattern_path
        for pattern_path in PATTERNS.glob("*.json")
    }
    if input_patterns:
        for in_pat in map(Path, input_patterns):
            if in_pat.is_dir():
                new_pats = {
                    inp.name.split(".")[0].lower(): inp
                    for inp in in_pat.glob("*.json")
                }
            else:
                new_pats = {in_pat.name.split(".")[0].lower(): in_pat}
            json_patterns.update(new_pats)

    xl = pd.ExcelFile(wkbk_paths[0])
    not_found = 0
    pattern_sheet = ""
    for sheet in xl.sheet_names:
        if sheet.lower() not in json_patterns.keys():
            not_found += 1
            if not_found == len(xl.sheet_names):
                warn_msg = (
                    'The Excel File "{0}" cannot be processed as none of the worksheets match a '
                    + "supported pattern type.").format(wkbk.parts[-1])
                patterns_msg = ("The currently supported " +
                                "patterns are: {0}".format([*json_patterns]))
                patts = ("New patterns may be added in the" +
                         " ingrid/src/model_processing/patterns directory")
                warnings.warn("\n" + warn_msg + "\n" + patterns_msg + "\n" +
                              patts)
                break
            else:
                continue
        else:
            pattern_sheet = sheet.lower()
            break
    if not pattern_sheet:
        raise RuntimeError("No matching pattern found nor provided." +
                           " Check the sheet names and try again.")

    pattern = json_patterns[pattern_sheet]

    manager = Manager(excel_path=wkbk_paths, json_path=[pattern])
    for evaluator in manager.evaluators:
        evaluator.rename_df_columns()
        evaluator.add_missing_columns()
        evaluator.to_property_di_graph()

    manager.get_pattern_graph_diff(out_directory=outpath)
    manager.changes_to_excel(out_directory=outpath)

    print("Comparison Complete")
Exemple #5
0
    def test_match_changes(self):
        manager = Manager(
            excel_path=[
                (DATA_DIRECTORY / "Composition_Diff_JSON_Baseline.xlsx"),
                (DATA_DIRECTORY / "Composition_Diff_JSON_Changed.xlsx"),
            ],
            json_path=[(PATTERNS / "Composition.json")],
        )

        orig_data = {
            "Component": [
                "Thruster Cluster Assembly",
                "Propellant Isolation Assembly",
                "Spacecraft",
            ],
            "Position": ["Thruster-1", "LV-3", "ME"],
            "Part": ["Small Thruster", "Latch Valve", "Main Engine"],
        }
        derived_A_lv3 = (
            "A_propellant isolation assembly qua lv-3 context_lv-3")
        derived_lv3 = "propellant isolation assembly qua lv-3 context"
        orig_ids = {
            "Element Name": [
                "Thruster Cluster Assembly",
                "Propellant Isolation Assembly",
                "Spacecraft",
                "Thruster-1",
                "LV-3",
                "ME",
                "Small Thruster",
                "Latch Valve",
                "Main Engine",
                derived_A_lv3,
                derived_lv3,
            ],
            "ID": ["_{0}".format(num) for num in range(100, 111)],
        }
        change_data = {
            "Component": [
                "Thruster Cluster Assembly",
                "Propellant Isolation Assembly",
                "Space Ship",
            ],
            "Position": ["Thruster-1", "SV-5", "ME"],
            "Part": ["Big Thruster", "Solenoid Valve", "Main Engine"],
        }
        change_renm_data = {"new name": ["SV-5"], "old name": ["LV-3"]}
        eval = manager.evaluators[0]
        eval1 = manager.evaluators[-1]
        eval.df = pd.DataFrame(data=orig_data)
        eval.df_ids = pd.DataFrame(data=orig_ids)
        eval.rename_df_columns()
        eval.add_missing_columns()
        eval.to_property_di_graph()
        pdg = eval.prop_di_graph

        eval1.df = pd.DataFrame(data=change_data)
        eval1.df_ids = pd.DataFrame(data=orig_ids)
        eval1.df_renames = pd.DataFrame(data=change_renm_data)
        eval1.df_renames.set_index("new name", inplace=True)
        eval1.rename_df_columns()
        eval1.add_missing_columns()
        eval1.to_property_di_graph()
        pdg1 = eval1.prop_di_graph

        add_edge = DiEdge(
            source=Vertex(name="b", id="200"),
            target=Vertex(name="c", id="201"),
            edge_attribute="orange",
        )
        del_edge = DiEdge(
            source=Vertex(name="song"),
            target=Vertex(name="tiger"),
            edge_attribute="blue",
        )

        eval_1_e_dict = pdg.edge_dict
        eval_1_e_dict.update({del_edge.named_edge_triple: del_edge})
        eval_2_e_dict = pdg1.edge_dict
        eval_2_e_dict.update({add_edge.named_edge_triple: add_edge})

        edge_set_one = eval.edge_set  # get baseline edge set
        edge_set_one.add(del_edge)
        edge_set_two = eval1.edge_set  # get the changed edge set
        edge_set_two.add(add_edge)

        # remove common edges
        # have to do this with named edges.
        edge_set_one_set = {edge.named_edge_triple for edge in edge_set_one}
        edge_set_two_set = {edge.named_edge_triple for edge in edge_set_two}

        # Remove edges common to each but preserve set integrity for
        # each evaluator
        eval_one_unmatched_named = list(
            edge_set_one_set.difference(edge_set_two_set))
        eval_two_unmatched_named = list(
            edge_set_two_set.difference(edge_set_one_set))

        # Organize edges in dictionary based on type (this goes on for
        # multiple lines)
        eval_one_unmatched = [
            eval_1_e_dict[edge] for edge in eval_one_unmatched_named
        ]
        eval_two_unmatched = [
            eval_2_e_dict[edge] for edge in eval_two_unmatched_named
        ]

        eval_one_unmatch_map = dict(
            (edge.edge_attribute, list()) for edge in eval_one_unmatched)
        eval_two_unmatch_map = dict(
            (edge.edge_attribute, list()) for edge in eval_two_unmatched)

        for edge in eval_one_unmatched:
            eval_one_unmatch_map[edge.edge_attribute].append(edge)
        for edge in eval_two_unmatched:
            eval_two_unmatch_map[edge.edge_attribute].append(edge)

        eval_one_unmatch_pref = {}
        eval_two_unmatch_pref = {}

        ance_keys_not_in_base = set(eval_two_unmatch_map.keys()).difference(
            set(eval_one_unmatch_map))

        eval_one_unmatch_pref["Added"] = []
        eval_one_unmatch_pref["Deleted"] = []
        for edge_type in ance_keys_not_in_base:
            eval_one_unmatch_pref["Added"].extend(
                eval_two_unmatch_map[edge_type])

        for edge in eval_one_unmatched:
            if edge.edge_attribute not in eval_two_unmatch_map.keys():
                eval_one_unmatch_pref["Deleted"].append(edge)
            else:
                eval_one_unmatch_pref[edge] = copy(
                    eval_two_unmatch_map[edge.edge_attribute])
        for edge in eval_two_unmatched:
            if edge.edge_attribute not in eval_one_unmatch_map.keys():
                eval_two_unmatch_pref[edge] = []
            else:
                eval_two_unmatch_pref[edge] = copy(
                    eval_one_unmatch_map[edge.edge_attribute])

        match_dict = match_changes(change_dict=eval_one_unmatch_pref)

        orig = [
            (
                "A_propellant isolation assembly qua lv-3 context_lv-3",
                "propellant isolation assembly qua lv-3 context",
                "memberEnd",
            ),
            ("LV-3", "Propellant Isolation Assembly", "owner"),
            (
                "propellant isolation assembly qua lv-3 context",
                "A_propellant isolation assembly qua lv-3 context_lv-3",
                "owner",
            ),
            ("A_spacecraft qua me context_me", "ME", "memberEnd"),
            ("ME", "Spacecraft", "owner"),
            ("Thruster-1", "Small Thruster", "type"),
            (
                "propellant isolation assembly qua lv-3 context",
                "Propellant Isolation Assembly",
                "type",
            ),
            ("LV-3", "Latch Valve", "type"),
            (
                "A_propellant isolation assembly qua lv-3 context_lv-3",
                "LV-3",
                "memberEnd",
            ),
        ]
        change = [
            (
                "A_propellant isolation assembly qua sv-5 context_sv-5",
                "propellant isolation assembly qua sv-5 context",
                "memberEnd",
            ),
            ("SV-5", "Propellant Isolation Assembly", "owner"),
            (
                "propellant isolation assembly qua sv-5 context",
                "A_propellant isolation assembly qua sv-5 context_sv-5",
                "owner",
            ),
            ("A_space ship qua me context_me", "ME", "memberEnd"),
            ("ME", "Space Ship", "owner"),
            ("Thruster-1", "Big Thruster", "type"),
            (
                "propellant isolation assembly qua sv-5 context",
                "Propellant Isolation Assembly",
                "type",
            ),
            ("SV-5", "Solenoid Valve", "type"),
            (
                "A_propellant isolation assembly qua sv-5 context_sv-5",
                "SV-5",
                "memberEnd",
            ),
        ]
        expected_matches = {z[0]: z[1] for z in zip(orig, change)}

        expected_matches.update({
            "Added": [("b", "c", "orange")],
            "Deleted": [("song", "tiger", "blue")],
        })

        expected_unstable = {
            ("s1", "t1", "type"): [
                ("as1", "t1", "type"),
                ("s1", "at1", "type"),
            ]
        }
        pairings = match_dict[0]
        unstable_pairs = match_dict[1]
        pairings_str = {}
        pairings_str.update({"Deleted": []})
        pairings_str.update({"Added": []})

        unstable_keys = set(unstable_pairs.keys()).intersection(
            set(pairings.keys()))

        for key in pairings.keys():
            if key in unstable_keys:
                continue
            elif key not in ("Deleted", "Added"):
                pairings_str.update({
                    key.named_edge_triple:
                    pairings[key][0].named_edge_triple
                })
            else:
                for edge in pairings[key]:
                    pairings_str[key].append(edge.named_edge_triple)

        self.assertDictEqual(expected_matches, pairings_str)

        for key in unstable_keys:
            unstable_key_vals = {
                edge.named_edge_triple
                for edge in unstable_pairs[key]
            }
            self.assertEqual(
                set(expected_unstable[key.named_edge_triple]),
                unstable_key_vals,
            )
    def test_get_pattern_graph_diff(self):
        manager = Manager(
            excel_path=[
                DATA_DIRECTORY / "Composition Example.xlsx" for i in range(2)
            ],
            json_path=[PATTERNS / "Composition.json"],
        )
        # Create the actual graph object because get_pattern_graph_diff
        # employs the graph object properties
        # with 2 different original edges of the same type I can induce a
        # match based on rename and an unstable pair.
        og_eval = manager.evaluators[0]
        og_graph = PropertyDiGraph()
        og_eval.prop_di_graph = og_graph
        ch_eval = manager.evaluators[1]
        ch_graph = PropertyDiGraph()
        ch_eval.prop_di_graph = ch_graph
        with tempfile.TemporaryDirectory() as tmpdir:
            tmpdir = Path(tmpdir)
            orig_edge = DiEdge(
                source=Vertex(name="Car", id="_001"),
                target=Vertex(name="car", id="_002"),
                edge_attribute="type",
            )
            renm_source = DiEdge(
                source=Vertex(
                    name="Subaru",
                    id="_001",
                    original_name="Car",
                    original_id="_001",
                    node_types=["Atomic Thing"],
                ),
                target=Vertex(name="car", id="_002"),
                edge_attribute="type",
            )
            orig_edge2 = DiEdge(
                source=Vertex(name="Car", id="_001"),
                target=Vertex(name="Vehicle", id="_003"),
                edge_attribute="type",
            )
            unstab_edge1 = DiEdge(
                source=Vertex(name="Car", id="_001"),
                target=Vertex(name="Not Car", id="_100"),
                edge_attribute="type",
            )
            unstab_edge2 = DiEdge(
                source=Vertex(name="Cup", id="_101"),
                target=Vertex(name="Vehicle", id="_003"),
                edge_attribute="type",
            )
            added_edge = DiEdge(
                source=Vertex(
                    name="New Source",
                    id=uuid.uuid4(),
                    node_types=["Atomic Thing"],
                ),
                target=Vertex(
                    name="New Target",
                    id=uuid.uuid4(),
                    node_types=["Atomic Thing"],
                ),
                edge_attribute="newEdge",
            )
            del_edge = DiEdge(
                source=Vertex(name="Old Source", id="_010"),
                target=Vertex(name="Old Target", id="_011"),
                edge_attribute="oldEdge",
            )
            original_edges = [orig_edge, orig_edge2, del_edge]
            change_edge = [
                renm_source,
                unstab_edge1,
                unstab_edge2,
                added_edge,
            ]
            orig_attrs = [{
                "diedge": edge,
                "edge_attribute": edge.edge_attribute
            } for edge in original_edges]
            change_attrs = [{
                "diedge": edge,
                "edge_attribute": edge.edge_attribute
            } for edge in change_edge]
            for edge in zip(original_edges, orig_attrs):
                og_graph.add_node(edge[0].source.name,
                                  **{edge[0].source.name: edge[0].source})
                og_graph.add_node(edge[0].target.name,
                                  **{edge[0].target.name: edge[0].target})
                og_graph.add_edge(edge[0].source.name, edge[0].target.name,
                                  **edge[1])
            for edge in zip(change_edge, change_attrs):
                ch_graph.add_node(edge[0].source.name,
                                  **{edge[0].source.name: edge[0].source})
                ch_graph.add_node(edge[0].target.name,
                                  **{edge[0].target.name: edge[0].target})
                ch_graph.add_edge(edge[0].source.name, edge[0].target.name,
                                  **edge[1])

            ch_dict = manager.get_pattern_graph_diff(out_directory=tmpdir)

            ch_dict = ch_dict["0-1"]

            changes = ch_dict["Changes"]
            add = changes["Added"]  # a list
            deld = changes["Deleted"]  # a list
            unstab = ch_dict["Unstable Pairs"]  # DiEdge: [DiEdge ...]
            unstab[orig_edge2] = set(unstab[orig_edge2])
            change = changes[orig_edge]

            assert change[0] == renm_source
            # TODO: Find new edges if type is not found in original and if
            # the edge is composed of at least one new model element.
            assert add == [added_edge, added_edge]
            assert deld == [del_edge]
            assert unstab == {
                orig_edge2: {unstab_edge1, renm_source, unstab_edge2}
            }
    def test_graph_difference_to_json(self):
        manager = Manager(
            excel_path=[
                DATA_DIRECTORY / "Composition Example.xlsx" for i in range(2)
            ],
            json_path=[PATTERNS / "Composition.json"],
        )
        tr = manager.translator[0]
        with tempfile.TemporaryDirectory() as tmpdir:
            tmpdir = Path(tmpdir)
            orig_edge = DiEdge(
                source=Vertex(name="Car", id="_001"),
                target=Vertex(name="car", id="_002"),
                edge_attribute="type",
            )
            renm_source = DiEdge(
                source=Vertex(
                    name="Subaru",
                    id="_001",
                    original_name="Car",
                    original_id="_001",
                    node_types=["Atomic Thing"],
                ),
                target=Vertex(name="car", id="_002"),
                edge_attribute="type",
            )
            orig_edge2 = DiEdge(
                source=Vertex(name="Car", id="_001"),
                target=Vertex(name="Vehicle", id="_003"),
                edge_attribute="type",
            )
            renm_target = DiEdge(
                source=Vertex(name="Car", id="_001"),
                target=Vertex(
                    name="vehicle",
                    id="_003",
                    original_name="Vehicle",
                    original_id="_003",
                    node_types=["Composite Thing"],
                ),
                edge_attribute="type",
            )
            orig_edge3 = DiEdge(
                source=Vertex(name="subaru", id="_004"),
                target=Vertex(name="Vehicle", id="_005"),
                edge_attribute="type",
            )
            renm_both = DiEdge(
                source=Vertex(
                    name="Subaru",
                    id="_004",
                    original_name="subaru",
                    original_id="_004",
                    node_types=["composite owner"],
                ),
                target=Vertex(
                    name="vehicle",
                    id="_005",
                    original_name="Vehicle",
                    original_id="_005",
                    node_types=["Atomic Thing"],
                ),
                edge_attribute="type",
            )
            orig_edge4 = DiEdge(
                source=Vertex(name="subaru", id="_004"),
                target=Vertex(name="car", id="_002"),
                edge_attribute="type",
            )
            new_source = DiEdge(
                source=Vertex(
                    name="Subaru",
                    id=uuid.uuid4(),
                    node_types=["Composite Thing"],
                ),
                target=Vertex(name="car", id="_002"),
                edge_attribute="type",
            )
            orig_edge5 = DiEdge(
                source=Vertex(name="Car", id="_001"),
                target=Vertex(name="Vehicle", id="_005"),
                edge_attribute="type",
            )
            new_target = DiEdge(
                source=Vertex(name="Car", id="_001"),
                target=Vertex(
                    name="vehicle",
                    id=uuid.uuid4(),
                    node_types=["Atomic Thing"],
                ),
                edge_attribute="type",
            )
            orig_edge6 = DiEdge(
                source=Vertex(name="Car", id="_007"),
                target=Vertex(name="Vehicle", id="_005"),
                edge_attribute="type",
            )
            new_sub = Vertex(name="Subaru",
                             id=uuid.uuid4(),
                             node_types=["Composite Thing"])
            sub_cons = {
                "successors": [{
                    "source": "Subaru",
                    "target": "Car",
                    "edge_attribute": "type",
                }]
            }
            new_sub.successors = sub_cons
            new_both = DiEdge(
                source=Vertex(
                    name="Subaru",
                    id=uuid.uuid4(),
                    node_types=["Composite Thing"],
                ),
                target=Vertex(
                    name="vehicle",
                    id=uuid.uuid4(),
                    node_types=["Atomic Thing"],
                ),
                edge_attribute="type",
            )
            added_edge = DiEdge(
                source=Vertex(
                    name="New Source",
                    id=uuid.uuid4(),
                    node_types=["Atomic Thing"],
                ),
                target=Vertex(
                    name="New Target",
                    id=uuid.uuid4(),
                    node_types=["Atomic Thing"],
                ),
                edge_attribute="newEdge",
            )
            del_edge = DiEdge(
                source=Vertex(name="Old Source", id="_010"),
                target=Vertex(name="Old Target", id="_011"),
                edge_attribute="oldEdge",
            )
            change_dict = {
                orig_edge: [renm_source],
                orig_edge2: [renm_target],
                orig_edge3: [renm_both],
                orig_edge4: [new_source],
                orig_edge5: [new_target],
                orig_edge6: [new_both],
                "Added": [added_edge],
                "Deleted": [del_edge],
            }

            changes = manager.graph_difference_to_json(
                change_dict=change_dict,
                evaluators="0-1",
                translator=tr,
                out_directory=tmpdir,
            )

            rename = 0
            replace = 0
            create = 0
            delete = 0
            fall_through_ops = []
            for item in changes:
                op = item["ops"][0]["op"]
                if op == "create":
                    create += 1
                elif op == "replace":
                    replace += 1
                elif op == "rename":
                    rename += 1
                elif op == "delete":
                    delete += 1
                else:
                    fall_through_ops.append(op)
            # expect 4 node Renames
            # expect 7 edge replaces (1 is from add edge)
            # expect 6 node creates
            # expect 1 delete
            assert (rename == 4 and replace == 7 and create == 6
                    and delete == 1)
            assert not fall_through_ops
    def test_changes_to_excel(self):
        manager = Manager(
            excel_path=[
                DATA_DIRECTORY / "Composition Example.xlsx" for i in range(1)
            ],
            json_path=[PATTERNS / "Composition.json"],
        )
        og_edge = DiEdge(
            source=Vertex(name="green"),
            target=Vertex(name="apple"),
            edge_attribute="fruit",
        )
        change_edge = DiEdge(
            source=Vertex(name="gala"),
            target=Vertex(name="apple"),
            edge_attribute="fruit",
        )
        added_edge = DiEdge(
            source=Vertex(name="blueberry"),
            target=Vertex(name="berry"),
            edge_attribute="bush",
        )
        deleted_edge = DiEdge(
            source=Vertex(name="yellow"),
            target=Vertex(name="delicious"),
            edge_attribute="apple",
        )
        unstable_key = DiEdge(
            source=Vertex(name="tomato"),
            target=Vertex(name="fruit"),
            edge_attribute="fruit",
        )
        unstable_one = DiEdge(
            source=Vertex(name="tomato"),
            target=Vertex(name="vegetable"),
            edge_attribute="fruit",
        )
        unstable_two = DiEdge(
            source=Vertex(name="tomahto"),
            target=Vertex(name="fruit"),
            edge_attribute="fruit",
        )

        fake_datas = {
            "0-1": {
                "Changes": {
                    "Added": [added_edge],
                    "Deleted": [deleted_edge],
                    og_edge: [change_edge],
                },
                "Unstable Pairs": {
                    unstable_key: [unstable_one, unstable_two]
                },
            }
        }
        manager.evaluator_change_dict = fake_datas
        with tempfile.TemporaryDirectory() as tmpdir:
            outdir = Path(tmpdir)
            manager.changes_to_excel(out_directory=outdir)

            created_file_name = list(outdir.glob("*.xlsx"))[0]
            created_file = OUTPUT_DIRECTORY / created_file_name
            created_df = pd.read_excel(created_file)
            created_dict = created_df.to_dict()

            expected_data = {
                "Edit 1": ["('green', 'apple', 'fruit')"],
                "Edit 2": ["('gala', 'apple', 'fruit')"],
                "Unstable Matches Original": [
                    "('tomato', 'fruit', 'fruit')",
                    "('tomato', 'fruit', 'fruit')",
                ],
                "Unstable Matches Change": [
                    "('tomato', 'vegetable', 'fruit')",
                    "('tomahto', 'fruit', 'fruit')",
                ],
                "Added": ["('blueberry', 'berry', 'bush')"],
                "Deleted": ["('yellow', 'delicious', 'apple')"],
            }

            expected_df = pd.DataFrame(
                data=dict([(k, pd.Series(v))
                           for k, v in expected_data.items()]))
            expected_dict = expected_df.to_dict()

            self.assertDictEqual(expected_dict, created_dict)
            self.assertTrue(expected_df.equals(created_df))