From 6e1a9755244038bc25cc530ca2c05ae339dea64d Mon Sep 17 00:00:00 2001 From: Brian Ward Date: Wed, 30 Oct 2024 20:37:50 +0000 Subject: [PATCH 1/3] Indicate origin of generated data --- gui/src/app/Project/ProjectDataModel.ts | 13 ++- gui/src/app/Project/ProjectReducer.ts | 5 ++ gui/src/app/Project/ProjectSerialization.ts | 2 +- .../Scripting/DataGeneration/DataPyWindow.tsx | 2 +- .../Scripting/DataGeneration/DataRWindow.tsx | 2 +- .../DataGeneration/useDataGenState.ts | 11 ++- gui/src/app/pages/HomePage/HomePage.tsx | 89 ++++++++++++++----- gui/test/app/Project/ProjectDataModel.test.ts | 18 ++++ 8 files changed, 115 insertions(+), 27 deletions(-) diff --git a/gui/src/app/Project/ProjectDataModel.ts b/gui/src/app/Project/ProjectDataModel.ts index e85ef8f0..23eab707 100644 --- a/gui/src/app/Project/ProjectDataModel.ts +++ b/gui/src/app/Project/ProjectDataModel.ts @@ -90,13 +90,24 @@ const isProjectBase = (x: any): x is ProjectBase => { return true; }; +export enum DataSource { + GENERATED_BY_R = "generated_by_r", + GENERATED_BY_PYTHON = "generated_by_python", +} + type ProjectMetadata = { title: string; + dataSource?: DataSource; }; export const isProjectMetaData = (x: any): x is ProjectMetadata => { if (!baseObjectCheck(x)) return false; if (typeof x.title !== "string") return false; + if ( + x.dataSource !== undefined && // allow undefined for backwards compatibility + !Object.values(DataSource).includes(x.dataSource) + ) + return false; return true; }; @@ -128,7 +139,7 @@ export const isProjectDataModel = (x: any): x is ProjectDataModel => { export type ProjectPersistentDataModel = Omit; export const initialDataModel: ProjectDataModel = { - meta: { title: "Untitled" }, + meta: { title: "Untitled", dataSource: undefined }, ephemera: { stanFileContent: "", dataFileContent: "", diff --git a/gui/src/app/Project/ProjectReducer.ts b/gui/src/app/Project/ProjectReducer.ts index 4a2945d2..032d8cd0 100644 --- a/gui/src/app/Project/ProjectReducer.ts +++ b/gui/src/app/Project/ProjectReducer.ts @@ -1,5 +1,6 @@ import { FieldsContentsMap } from "@SpCore/FileMapping"; import { + DataSource, initialDataModel, ProjectDataModel, ProjectKnownFiles, @@ -24,6 +25,7 @@ export type ProjectReducerAction = type: "retitle"; title: string; } + | { type: "setDataSource"; dataSource: DataSource | undefined } | { type: "editFile"; content: string; @@ -81,6 +83,9 @@ const ProjectReducer = (s: ProjectDataModel, a: ProjectReducerAction) => { case "clear": { return initialDataModel; } + case "setDataSource": { + return { ...s, meta: { ...s.meta, dataSource: a.dataSource } }; + } default: return unreachable(a); } diff --git a/gui/src/app/Project/ProjectSerialization.ts b/gui/src/app/Project/ProjectSerialization.ts index c9cfd106..19124665 100644 --- a/gui/src/app/Project/ProjectSerialization.ts +++ b/gui/src/app/Project/ProjectSerialization.ts @@ -112,7 +112,7 @@ const loadMetaFromString = ( json: string, clearExisting: boolean = false, ): ProjectDataModel => { - const newMeta = JSON.parse(json); + let newMeta = JSON.parse(json); if (!isProjectMetaData(newMeta)) { throw Error("Deserialized meta is not valid"); } diff --git a/gui/src/app/Scripting/DataGeneration/DataPyWindow.tsx b/gui/src/app/Scripting/DataGeneration/DataPyWindow.tsx index bd6f5241..4f2f8bf0 100644 --- a/gui/src/app/Scripting/DataGeneration/DataPyWindow.tsx +++ b/gui/src/app/Scripting/DataGeneration/DataPyWindow.tsx @@ -22,7 +22,7 @@ const handleHelp = () => ); const DataPyWindow: FunctionComponent = () => { - const { consoleRef, status, onStatus, onData } = useDataGenState(); + const { consoleRef, status, onStatus, onData } = useDataGenState("python"); const callbacks = useMemo( () => ({ diff --git a/gui/src/app/Scripting/DataGeneration/DataRWindow.tsx b/gui/src/app/Scripting/DataGeneration/DataRWindow.tsx index b383932e..d37ae269 100644 --- a/gui/src/app/Scripting/DataGeneration/DataRWindow.tsx +++ b/gui/src/app/Scripting/DataGeneration/DataRWindow.tsx @@ -19,7 +19,7 @@ const handleHelp = () => ); const DataRWindow: FunctionComponent = () => { - const { consoleRef, status, onStatus, onData } = useDataGenState(); + const { consoleRef, status, onStatus, onData } = useDataGenState("r"); const { run } = useWebR({ consoleRef, onStatus, onData }); const handleRun = useCallback( diff --git a/gui/src/app/Scripting/DataGeneration/useDataGenState.ts b/gui/src/app/Scripting/DataGeneration/useDataGenState.ts index 892d8597..25c74cc5 100644 --- a/gui/src/app/Scripting/DataGeneration/useDataGenState.ts +++ b/gui/src/app/Scripting/DataGeneration/useDataGenState.ts @@ -1,12 +1,12 @@ import { useCallback, useContext, useRef, useState } from "react"; -import { ProjectKnownFiles } from "@SpCore/ProjectDataModel"; +import { DataSource, ProjectKnownFiles } from "@SpCore/ProjectDataModel"; import { writeConsoleOutToDiv } from "@SpScripting/OutputDivUtils"; import { InterpreterStatus } from "@SpScripting/InterpreterTypes"; import { ProjectContext } from "@SpCore/ProjectContextProvider"; // A custom hook to share logic between the Python and R data generation windows // This contains the output div ref, the interpreter state, and the callback to update the data. -const useDataGenState = () => { +const useDataGenState = (source: "python" | "r") => { const [status, setStatus] = useState("idle"); const consoleRef = useRef(null); @@ -26,6 +26,13 @@ const useDataGenState = () => { filename: ProjectKnownFiles.DATAFILE, }); update({ type: "commitFile", filename: ProjectKnownFiles.DATAFILE }); + update({ + type: "setDataSource", + dataSource: + source === "python" + ? DataSource.GENERATED_BY_PYTHON + : DataSource.GENERATED_BY_R, + }); // Use "stan-playground" prefix to distinguish from console output of the running code writeConsoleOutToDiv( consoleRef, diff --git a/gui/src/app/pages/HomePage/HomePage.tsx b/gui/src/app/pages/HomePage/HomePage.tsx index 6e1ffa52..b1202c9c 100644 --- a/gui/src/app/pages/HomePage/HomePage.tsx +++ b/gui/src/app/pages/HomePage/HomePage.tsx @@ -8,6 +8,7 @@ import TextEditor from "@SpComponents/TextEditor"; import { FileNames } from "@SpCore/FileMapping"; import { ProjectContext } from "@SpCore/ProjectContextProvider"; import { + DataSource, modelHasUnsavedChanges, ProjectKnownFiles, } from "@SpCore/ProjectDataModel"; @@ -17,12 +18,15 @@ import DataPyWindow from "@SpScripting/DataGeneration/DataPyWindow"; import DataRWindow from "@SpScripting/DataGeneration/DataRWindow"; import { FunctionComponent, + useCallback, useContext, useEffect, + useMemo, useRef, useState, } from "react"; import SamplingWindow from "./SamplingWindow/SamplingWindow"; +import { ToolbarItem } from "@SpComponents/ToolBar"; type Props = { // @@ -92,6 +96,7 @@ type LeftViewProps = { const LeftView: FunctionComponent = () => { const { data, update } = useContext(ProjectContext); + return ( = () => { } readOnly={false} /> - - update({ - type: "commitFile", - filename: ProjectKnownFiles.DATAFILE, - }) - } - editedText={data.ephemera.dataFileContent} - onSetEditedText={(content: string) => - update({ - type: "editFile", - content, - filename: ProjectKnownFiles.DATAFILE, - }) - } - readOnly={false} - contentOnEmpty={"Enter JSON data or use the data generation tab"} - /> + ); }; +const DataEditor: FunctionComponent<{}> = () => { + const { data, update } = useContext(ProjectContext); + + const dataIsEdited = useMemo(() => { + return data.dataFileContent !== data.ephemera.dataFileContent; + }, [data.dataFileContent, data.ephemera.dataFileContent]); + + const dataMessage: ToolbarItem[] = useMemo(() => { + if (data.meta.dataSource === undefined || dataIsEdited) { + return []; + } else { + return [ + { + type: "text", + label: + "Data is generated by data." + + (data.meta.dataSource === DataSource.GENERATED_BY_PYTHON + ? "py" + : "R"), + color: "info.main", + }, + ]; + } + }, [data.meta.dataSource, dataIsEdited]); + + const onSetEditedText = useCallback( + (content: string) => { + update({ + type: "editFile", + content, + filename: ProjectKnownFiles.DATAFILE, + }); + }, + [update], + ); + + const onSaveText = useCallback(() => { + update({ + type: "commitFile", + filename: ProjectKnownFiles.DATAFILE, + }); + update({ + type: "setDataSource", + dataSource: undefined, + }); + }, [update]); + + return ( + + ); +}; + // adapted from https://mui.com/material-ui/react-drawer/#persistent-drawer const MovingBox = styled(Box, { shouldForwardProp: (prop) => prop !== "open", diff --git a/gui/test/app/Project/ProjectDataModel.test.ts b/gui/test/app/Project/ProjectDataModel.test.ts index 73dc7e70..1783488b 100644 --- a/gui/test/app/Project/ProjectDataModel.test.ts +++ b/gui/test/app/Project/ProjectDataModel.test.ts @@ -1,4 +1,5 @@ import { + DataSource, defaultSamplingOpts, exportedForTesting, getStringKnownFileKeys, @@ -248,6 +249,23 @@ describe("Project data model type guards", () => { expect(isProjectMetaData({ title: 6 })).toBe(false); expect(isProjectMetaData({ no_title: "title" })).toBe(false); }); + test("Returns true for valid data source", () => { + expect( + isProjectMetaData({ + title: "title", + dataSource: DataSource.GENERATED_BY_PYTHON, + }), + ).toBe(true); + expect( + isProjectMetaData({ + title: "title", + dataSource: DataSource.GENERATED_BY_R, + }), + ).toBe(true); + }); + test("Returns false on bad data source", () => { + expect(isProjectMetaData({ title: "title", dataSource: 1 })).toBe(false); + }); }); describe("Project ephemeral-data typeguard", () => { test("Returns true for valid project files object", () => { From f67c9a19d3d285c74c74015ee406fceca717e15c Mon Sep 17 00:00:00 2001 From: Brian Ward Date: Wed, 30 Oct 2024 20:48:29 +0000 Subject: [PATCH 2/3] Simplify reducer update --- gui/src/app/Project/ProjectReducer.ts | 19 +++++++++++---- .../DataGeneration/useDataGenState.ts | 9 ++------ gui/src/app/pages/HomePage/HomePage.tsx | 4 ---- gui/test/app/Project/ProjectReducer.test.ts | 23 +++++++++++++++++++ 4 files changed, 40 insertions(+), 15 deletions(-) diff --git a/gui/src/app/Project/ProjectReducer.ts b/gui/src/app/Project/ProjectReducer.ts index 032d8cd0..e791c27b 100644 --- a/gui/src/app/Project/ProjectReducer.ts +++ b/gui/src/app/Project/ProjectReducer.ts @@ -25,7 +25,7 @@ export type ProjectReducerAction = type: "retitle"; title: string; } - | { type: "setDataSource"; dataSource: DataSource | undefined } + | { type: "generateData"; content: string; dataSource: DataSource } | { type: "editFile"; content: string; @@ -71,9 +71,23 @@ const ProjectReducer = (s: ProjectDataModel, a: ProjectReducerAction) => { } case "commitFile": { const newState = { ...s }; + if (a.filename === ProjectKnownFiles.DATAFILE) { + newState.meta = { ...s.meta, dataSource: undefined }; + } newState[a.filename] = s.ephemera[a.filename]; return newState; } + case "generateData": { + return { + ...s, + [ProjectKnownFiles.DATAFILE]: a.content, + ephemera: { + ...s.ephemera, + [ProjectKnownFiles.DATAFILE]: a.content, + }, + meta: { ...s.meta, dataSource: a.dataSource }, + }; + } case "setSamplingOpts": { return { ...s, samplingOpts: { ...s.samplingOpts, ...a.opts } }; } @@ -83,9 +97,6 @@ const ProjectReducer = (s: ProjectDataModel, a: ProjectReducerAction) => { case "clear": { return initialDataModel; } - case "setDataSource": { - return { ...s, meta: { ...s.meta, dataSource: a.dataSource } }; - } default: return unreachable(a); } diff --git a/gui/src/app/Scripting/DataGeneration/useDataGenState.ts b/gui/src/app/Scripting/DataGeneration/useDataGenState.ts index 25c74cc5..830918bb 100644 --- a/gui/src/app/Scripting/DataGeneration/useDataGenState.ts +++ b/gui/src/app/Scripting/DataGeneration/useDataGenState.ts @@ -1,5 +1,5 @@ import { useCallback, useContext, useRef, useState } from "react"; -import { DataSource, ProjectKnownFiles } from "@SpCore/ProjectDataModel"; +import { DataSource } from "@SpCore/ProjectDataModel"; import { writeConsoleOutToDiv } from "@SpScripting/OutputDivUtils"; import { InterpreterStatus } from "@SpScripting/InterpreterTypes"; import { ProjectContext } from "@SpCore/ProjectContextProvider"; @@ -21,13 +21,8 @@ const useDataGenState = (source: "python" | "r") => { if (dataJson !== lastData.current) { lastData.current = dataJson; update({ - type: "editFile", + type: "generateData", content: dataJson, - filename: ProjectKnownFiles.DATAFILE, - }); - update({ type: "commitFile", filename: ProjectKnownFiles.DATAFILE }); - update({ - type: "setDataSource", dataSource: source === "python" ? DataSource.GENERATED_BY_PYTHON diff --git a/gui/src/app/pages/HomePage/HomePage.tsx b/gui/src/app/pages/HomePage/HomePage.tsx index b1202c9c..c4072967 100644 --- a/gui/src/app/pages/HomePage/HomePage.tsx +++ b/gui/src/app/pages/HomePage/HomePage.tsx @@ -165,10 +165,6 @@ const DataEditor: FunctionComponent<{}> = () => { type: "commitFile", filename: ProjectKnownFiles.DATAFILE, }); - update({ - type: "setDataSource", - dataSource: undefined, - }); }, [update]); return ( diff --git a/gui/test/app/Project/ProjectReducer.test.ts b/gui/test/app/Project/ProjectReducer.test.ts index 04468cc5..b8681466 100644 --- a/gui/test/app/Project/ProjectReducer.test.ts +++ b/gui/test/app/Project/ProjectReducer.test.ts @@ -1,4 +1,5 @@ import { + DataSource, getStringKnownFileKeys, initialDataModel, ProjectDataModel, @@ -130,6 +131,7 @@ describe("Project reducer", () => { const initialState = { ...permanentFiles, ephemera: { ...ephemeralFiles }, + meta: { dataSource: DataSource.GENERATED_BY_PYTHON }, } as any as ProjectDataModel; const commitAction: ProjectReducerAction = { type: "commitFile", @@ -143,6 +145,7 @@ describe("Project reducer", () => { expect(result[ProjectKnownFiles.DATAFILE]).toEqual( result.ephemera[ProjectKnownFiles.DATAFILE], ); + expect(result.meta.dataSource).toBeUndefined(); }); test("Save action does not save non-chosen files", () => { const result = ProjectReducer(initialState, commitAction); @@ -220,4 +223,24 @@ describe("Project reducer", () => { expect(result).toBe(initialDataModel); }); }); + + describe("Generate data", () => { + test("Updates data file and ephemera with new data", () => { + const initialState = { + ...permanentFiles, + ephemera: { ...ephemeralFiles }, + meta: { dataSource: DataSource.GENERATED_BY_R }, + } as any as ProjectDataModel; + const newData = "generated data"; + const generateAction: ProjectReducerAction = { + type: "generateData", + content: newData, + dataSource: DataSource.GENERATED_BY_PYTHON, + }; + const result = ProjectReducer(initialState, generateAction); + expect(result[ProjectKnownFiles.DATAFILE]).toEqual(newData); + expect(result.ephemera[ProjectKnownFiles.DATAFILE]).toEqual(newData); + expect(result.meta.dataSource).toEqual(DataSource.GENERATED_BY_PYTHON); + }); + }); }); From fa1c0e949e6c88e7053ec31e59c94a24d0e631bb Mon Sep 17 00:00:00 2001 From: Jeff Soules Date: Mon, 4 Nov 2024 14:16:09 +0000 Subject: [PATCH 3/3] Label case where data is from a prior version of generator script. (#237) * Label stale data from prior generator-script versions. * Ensure exhaustive check for describing data source type. Add color. * Remove central list of warning-sources; include color and msg logic in same switch. --- gui/src/app/Project/ProjectDataModel.ts | 4 +- gui/src/app/Project/ProjectReducer.ts | 29 ++++++++++- gui/src/app/Project/ProjectSerialization.ts | 2 +- gui/src/app/pages/HomePage/HomePage.tsx | 42 ++++++++++++---- gui/test/app/Project/ProjectReducer.test.ts | 55 +++++++++++++++++++++ 5 files changed, 118 insertions(+), 14 deletions(-) diff --git a/gui/src/app/Project/ProjectDataModel.ts b/gui/src/app/Project/ProjectDataModel.ts index 23eab707..c9c048e0 100644 --- a/gui/src/app/Project/ProjectDataModel.ts +++ b/gui/src/app/Project/ProjectDataModel.ts @@ -92,7 +92,9 @@ const isProjectBase = (x: any): x is ProjectBase => { export enum DataSource { GENERATED_BY_R = "generated_by_r", + GENERATED_BY_STALE_R = "generated_by_stale_r", GENERATED_BY_PYTHON = "generated_by_python", + GENERATED_BY_STALE_PYTHON = "generated_by_stale_python", } type ProjectMetadata = { @@ -104,7 +106,7 @@ export const isProjectMetaData = (x: any): x is ProjectMetadata => { if (!baseObjectCheck(x)) return false; if (typeof x.title !== "string") return false; if ( - x.dataSource !== undefined && // allow undefined for backwards compatibility + x.dataSource !== undefined && // undefined = manually edited or unknown provenance !Object.values(DataSource).includes(x.dataSource) ) return false; diff --git a/gui/src/app/Project/ProjectReducer.ts b/gui/src/app/Project/ProjectReducer.ts index e791c27b..1a0d04f4 100644 --- a/gui/src/app/Project/ProjectReducer.ts +++ b/gui/src/app/Project/ProjectReducer.ts @@ -71,8 +71,12 @@ const ProjectReducer = (s: ProjectDataModel, a: ProjectReducerAction) => { } case "commitFile": { const newState = { ...s }; - if (a.filename === ProjectKnownFiles.DATAFILE) { - newState.meta = { ...s.meta, dataSource: undefined }; + const newDataSource = confirmDataSourceForCommit( + s.meta.dataSource, + a.filename, + ); + if (newDataSource !== s.meta.dataSource) { + newState.meta = { ...s.meta, dataSource: newDataSource }; } newState[a.filename] = s.ephemera[a.filename]; return newState; @@ -102,4 +106,25 @@ const ProjectReducer = (s: ProjectDataModel, a: ProjectReducerAction) => { } }; +const confirmDataSourceForCommit = ( + currentSource: DataSource | undefined, + editedFile: ProjectKnownFiles, +): DataSource | undefined => { + if (editedFile === ProjectKnownFiles.DATAFILE) return undefined; + if ( + editedFile === ProjectKnownFiles.DATAPYFILE && + currentSource === DataSource.GENERATED_BY_PYTHON + ) { + return DataSource.GENERATED_BY_STALE_PYTHON; + } + if ( + editedFile === ProjectKnownFiles.DATARFILE && + currentSource === DataSource.GENERATED_BY_R + ) { + return DataSource.GENERATED_BY_STALE_R; + } + + return currentSource; +}; + export default ProjectReducer; diff --git a/gui/src/app/Project/ProjectSerialization.ts b/gui/src/app/Project/ProjectSerialization.ts index 19124665..c9cfd106 100644 --- a/gui/src/app/Project/ProjectSerialization.ts +++ b/gui/src/app/Project/ProjectSerialization.ts @@ -112,7 +112,7 @@ const loadMetaFromString = ( json: string, clearExisting: boolean = false, ): ProjectDataModel => { - let newMeta = JSON.parse(json); + const newMeta = JSON.parse(json); if (!isProjectMetaData(newMeta)) { throw Error("Deserialized meta is not valid"); } diff --git a/gui/src/app/pages/HomePage/HomePage.tsx b/gui/src/app/pages/HomePage/HomePage.tsx index c4072967..16827824 100644 --- a/gui/src/app/pages/HomePage/HomePage.tsx +++ b/gui/src/app/pages/HomePage/HomePage.tsx @@ -5,6 +5,7 @@ import useMediaQuery from "@mui/material/useMediaQuery"; import StanFileEditor from "@SpComponents/StanFileEditor"; import TabWidget from "@SpComponents/TabWidget"; import TextEditor from "@SpComponents/TextEditor"; +import { ColorOptions, ToolbarItem } from "@SpComponents/ToolBar"; import { FileNames } from "@SpCore/FileMapping"; import { ProjectContext } from "@SpCore/ProjectContextProvider"; import { @@ -16,6 +17,7 @@ import Sidebar, { drawerWidth } from "@SpPages/Sidebar"; import TopBar from "@SpPages/TopBar"; import DataPyWindow from "@SpScripting/DataGeneration/DataPyWindow"; import DataRWindow from "@SpScripting/DataGeneration/DataRWindow"; +import { unreachable } from "@SpUtil/unreachable"; import { FunctionComponent, useCallback, @@ -26,7 +28,6 @@ import { useState, } from "react"; import SamplingWindow from "./SamplingWindow/SamplingWindow"; -import { ToolbarItem } from "@SpComponents/ToolBar"; type Props = { // @@ -124,30 +125,51 @@ const LeftView: FunctionComponent = () => { ); }; -const DataEditor: FunctionComponent<{}> = () => { +const DataEditor: FunctionComponent = () => { const { data, update } = useContext(ProjectContext); const dataIsEdited = useMemo(() => { return data.dataFileContent !== data.ephemera.dataFileContent; }, [data.dataFileContent, data.ephemera.dataFileContent]); + const dataSourceDesc: undefined | { msg: string; color: ColorOptions } = + useMemo(() => { + if (dataIsEdited) return undefined; + + switch (data.meta.dataSource) { + case undefined: { + return undefined; + } + case DataSource.GENERATED_BY_PYTHON: { + return { msg: "data.py", color: "info.main" }; + } + case DataSource.GENERATED_BY_R: { + return { msg: "data.R", color: "info.main" }; + } + case DataSource.GENERATED_BY_STALE_PYTHON: { + return { msg: "a prior version of data.py.", color: "warning.main" }; + } + case DataSource.GENERATED_BY_STALE_R: { + return { msg: "a prior version of data.R.", color: "warning.main" }; + } + default: + return unreachable(data.meta.dataSource); + } + }, [dataIsEdited, data.meta.dataSource]); + const dataMessage: ToolbarItem[] = useMemo(() => { - if (data.meta.dataSource === undefined || dataIsEdited) { + if (dataSourceDesc === undefined) { return []; } else { return [ { type: "text", - label: - "Data is generated by data." + - (data.meta.dataSource === DataSource.GENERATED_BY_PYTHON - ? "py" - : "R"), - color: "info.main", + label: `Data generated by ${dataSourceDesc.msg}`, + color: dataSourceDesc.color, }, ]; } - }, [data.meta.dataSource, dataIsEdited]); + }, [dataSourceDesc]); const onSetEditedText = useCallback( (content: string) => { diff --git a/gui/test/app/Project/ProjectReducer.test.ts b/gui/test/app/Project/ProjectReducer.test.ts index b8681466..6996783e 100644 --- a/gui/test/app/Project/ProjectReducer.test.ts +++ b/gui/test/app/Project/ProjectReducer.test.ts @@ -145,6 +145,12 @@ describe("Project reducer", () => { expect(result[ProjectKnownFiles.DATAFILE]).toEqual( result.ephemera[ProjectKnownFiles.DATAFILE], ); + }); + test("Saving data.json clears data source", () => { + expect(initialState[ProjectKnownFiles.DATAFILE]).not.toEqual( + initialState.ephemera[ProjectKnownFiles.DATAFILE], + ); + const result = ProjectReducer(initialState, commitAction); expect(result.meta.dataSource).toBeUndefined(); }); test("Save action does not save non-chosen files", () => { @@ -157,6 +163,55 @@ describe("Project reducer", () => { const result = ProjectReducer(initialState, commitAction); expect(result.ephemera).toEqual(initialState.ephemera); }); + test("Saving data generation script updates status on data it generated", () => { + const pairs = [ + { + source: DataSource.GENERATED_BY_PYTHON, + newSource: DataSource.GENERATED_BY_STALE_PYTHON, + file: ProjectKnownFiles.DATAPYFILE, + }, + { + source: DataSource.GENERATED_BY_R, + newSource: DataSource.GENERATED_BY_STALE_R, + file: ProjectKnownFiles.DATARFILE, + }, + ]; + pairs.forEach((p) => { + const initial = { + ...initialState, + meta: { dataSource: p.source }, + } as any as ProjectDataModel; + const commit = { ...commitAction, filename: p.file }; + const result = ProjectReducer(initial, commit); + expect(result.meta.dataSource).toEqual(p.newSource); + }); + }); + test("Saving data generation script does not change status for data.json it didn't generate", () => { + const pairs = [ + { + source: DataSource.GENERATED_BY_PYTHON, + file: ProjectKnownFiles.DATAPYFILE, + }, + { + source: DataSource.GENERATED_BY_R, + file: ProjectKnownFiles.DATARFILE, + }, + ]; + const sources = Object.entries(DataSource); + pairs.forEach((p) => { + sources + .filter(([, value]) => value !== p.source) + .forEach((s) => { + const initial = { + ...initialState, + meta: { dataSource: s }, + } as any as ProjectDataModel; + const commit = { ...commitAction, filename: p.file }; + const result = ProjectReducer(initial, commit); + expect(result.meta.dataSource).toEqual(s); + }); + }); + }); }); describe("Updating sampling options", () => {