diff --git a/.gitignore b/.gitignore index e1db096..ee6b67c 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,6 @@ .RData .Ruserdata _processedLockFile.lock +ModuleBundleBuildDir/ +bundles_out/ +results_multidataset.json diff --git a/NAMESPACE b/NAMESPACE index 080553b..c1e4a3f 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -1,3 +1,4 @@ exportPattern('^[[:alpha:]]+') + import('jaspBase'); import('svglite'); import('stringi'); diff --git a/NEWS.md b/NEWS.md index 361436b..c99c728 100644 --- a/NEWS.md +++ b/NEWS.md @@ -15,6 +15,34 @@ --- # jaspTestModule (development version) +## Added +* "Test Multi-DataSet": analysis + form to exercise multi-dataset aware analyses + (`multiDataSetAware` Description entries, the VariablesForm dataset/filter selection, + the `datasets` handout and encoded-name routing of jaspBase, the readDataSet() guard, + the `.meta` dataSetId/filterId provenance and the non-aware counterpart as controls). + The aware analysis is wrapped (`hasWrapper: true`): body in `multiDataSetFuncInternal`, + generated `multiDataSetFunc(datasets = ...)` wrapper for R syntax use. +* `Tools/buildBundle.R`: builds an installable `bundles_out/jaspTestModule.JASPModule` + from this checkout with the local `jasp-desktop/Engine/jaspBase` (or any checkout via + `JASP_BASE_DIR`) packed into the bundle; no JASP cellar needed. +* `Tools/testMultiDataSet.R` (+ `_wrapper.R`): full headless test of the analysis over + engine-style queued slices, the wrapper in a clean session and a legacy non-aware run. +* `Tools/testdata/DataSetA.csv` / `DataSetB.csv`: two datasets with deliberately colliding + column names for the per-dataset routing test. + +## Changed +* `renv.lock` re-resolved for R 4.6.1 (current CRAN, jaspBase/jaspGraphs from GitHub HEAD). +* `hasWrappers: true` for the module: every analysis body now lives in `Internal` + and has a generated `R/Wrapper.R` (via jaspSyntax/SyntaxInterface) exposing the + plain `(data = ..., ...)` entry point for R syntax use - 38 of 43; the four + entries pointing at removed qml files (test.qml, TestPlotStatus.qml) can't generate + one, and filteredDataEntry crashes the syntax bridge headlessly (Filter without + DataSet assertion in CommonData/filter.cpp - a real bridge bug to fix). +* The non-aware twin (`multiDataSetNonAware` func/qml/wrapper) is removed again (mds-plan + C4/M8): its generated wrapper carried a stale qml pointer so its R entry point always + errored, and every other analysis in this module already is a plain single-dataset one. + Legacy path 3 of Tools/testMultiDataSet.R now runs through `testEncodingFunc` (whose R body + really calls `.readDataSetToEnd`). diff --git a/R/AnalysisUpgradeAWrapper.R b/R/AnalysisUpgradeAWrapper.R new file mode 100644 index 0000000..7e3a88f --- /dev/null +++ b/R/AnalysisUpgradeAWrapper.R @@ -0,0 +1,103 @@ +# +# Copyright (C) 2013-2025 University of Amsterdam +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +# This is a generated file. Don't change it! + +#' Analysis A +#' +#' @param stemAndLeaf, Create a Stem and leaf table. +#' Defaults to \code{FALSE}. +#' @param stemAndLeafScale, The scale parameter controls how much the table is expanded. For example, scale = 2 will cause the table to be roughly twice as long as the default (scale = 1). +AnalysisUpgradeA <- function( + data = NULL, + version = "1", + addSmooth = TRUE, + addSmoothCI = TRUE, + addSmoothCIValue = 0.95, + binWidthType = "sturges", + chartType = "_2barCharts", + chartValues = "_2percentages", + colorPalette = "colorblind", + descriptivesIntervalPlot = FALSE, + descriptivesPiechart = FALSE, + descriptivesQQPlot = FALSE, + distPlotDensity = FALSE, + distPlotRug = FALSE, + frequencyTables = FALSE, + frequencyTablesMaximumAmount = 10, + graphTypeAbove = "density", + graphTypeRight = "density", + iqr = FALSE, + kurtosis = FALSE, + mad = FALSE, + madrobust = FALSE, + maximum = TRUE, + mean = TRUE, + median = FALSE, + minimum = TRUE, + mode = FALSE, + numberOfBins = 30, + percentileValuesEqualGroups = FALSE, + percentileValuesEqualGroupsNo = 4, + percentileValuesPercentiles = FALSE, + percentileValuesQuartiles = FALSE, + plotCorrelationMatrix = FALSE, + plotHeight = 320, + plotVariables = FALSE, + plotWidth = 480, + range = FALSE, + regressionType = "smooth", + scatterPlot = FALSE, + shapiro = FALSE, + showLegend = TRUE, + skewness = FALSE, + splitPlotBoxplot = TRUE, + splitPlotColour = FALSE, + splitPlotJitter = FALSE, + splitPlotOutlierLabel = FALSE, + splitPlotViolin = FALSE, + splitPlots = FALSE, + splitby = list(types = list(), value = ""), + standardDeviation = TRUE, + standardErrorMean = FALSE, + statisticsValuesAreGroupMidpoints = FALSE, + stemAndLeaf = FALSE, + stemAndLeafScale = 1, + sum = FALSE, + variables = list(types = list(), value = list()), + variance = FALSE) { + + defaultArgCalls <- formals(jaspTestModule::AnalysisUpgradeA) + defaultArgs <- lapply(defaultArgCalls, eval) + options <- as.list(match.call())[-1L] + options <- lapply(options, eval) + defaults <- setdiff(names(defaultArgs), names(options)) + options[defaults] <- defaultArgs[defaults] + options[["data"]] <- NULL + options[["version"]] <- NULL + + + if (!jaspBase::jaspResultsCalledFromJasp() && !is.null(data)) { + jaspBase::storeDataSet(data) + } + + optionsWithFormula <- c("binWidthType", "colorPalette", "splitby", "variables") + for (name in optionsWithFormula) { + if ((name %in% optionsWithFormula) && inherits(options[[name]], "formula")) options[[name]] = jaspBase::jaspFormula(options[[name]], data) } + + return(jaspBase::runWrappedAnalysis("jaspTestModule", "AnalysisUpgradeA", "AnalysisUpgrade.qml", options, version, FALSE)) +} \ No newline at end of file diff --git a/R/AnalysisUpgradeBWrapper.R b/R/AnalysisUpgradeBWrapper.R new file mode 100644 index 0000000..e58af93 --- /dev/null +++ b/R/AnalysisUpgradeBWrapper.R @@ -0,0 +1,103 @@ +# +# Copyright (C) 2013-2025 University of Amsterdam +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +# This is a generated file. Don't change it! + +#' Analysis B +#' +#' @param stemAndLeaf, Create a Stem and leaf table. +#' Defaults to \code{FALSE}. +#' @param stemAndLeafScale, The scale parameter controls how much the table is expanded. For example, scale = 2 will cause the table to be roughly twice as long as the default (scale = 1). +AnalysisUpgradeB <- function( + data = NULL, + version = "1", + addSmooth = TRUE, + addSmoothCI = TRUE, + addSmoothCIValue = 0.95, + binWidthType = "sturges", + chartType = "_1noCharts", + chartValues = "_1frequencies", + colorPalette = "colorblind", + descriptivesIntervalPlot = FALSE, + descriptivesPiechart = FALSE, + descriptivesQQPlot = FALSE, + distPlotDensity = FALSE, + distPlotRug = FALSE, + frequencyTables = FALSE, + frequencyTablesMaximumAmount = 10, + graphTypeAbove = "density", + graphTypeRight = "density", + iqr = FALSE, + kurtosis = FALSE, + mad = FALSE, + madrobust = FALSE, + maximum = TRUE, + mean = TRUE, + median = FALSE, + minimum = TRUE, + mode = FALSE, + numberOfBins = 30, + percentileValuesEqualGroups = FALSE, + percentileValuesEqualGroupsNo = 4, + percentileValuesPercentiles = FALSE, + percentileValuesQuartiles = FALSE, + plotCorrelationMatrix = FALSE, + plotHeight = 320, + plotVariables = FALSE, + plotWidth = 480, + range = FALSE, + regressionType = "smooth", + scatterPlot = FALSE, + shapiro = FALSE, + showLegend = TRUE, + skewness = FALSE, + splitPlotBoxplot = TRUE, + splitPlotColour = FALSE, + splitPlotJitter = FALSE, + splitPlotOutlierLabel = FALSE, + splitPlotViolin = FALSE, + splitPlots = FALSE, + splitby = list(types = list(), value = ""), + standardDeviation = TRUE, + standardErrorMean = FALSE, + statisticsValuesAreGroupMidpoints = FALSE, + stemAndLeaf = FALSE, + stemAndLeafScale = 1, + sum = FALSE, + variables = list(types = list(), value = list()), + variance = FALSE) { + + defaultArgCalls <- formals(jaspTestModule::AnalysisUpgradeB) + defaultArgs <- lapply(defaultArgCalls, eval) + options <- as.list(match.call())[-1L] + options <- lapply(options, eval) + defaults <- setdiff(names(defaultArgs), names(options)) + options[defaults] <- defaultArgs[defaults] + options[["data"]] <- NULL + options[["version"]] <- NULL + + + if (!jaspBase::jaspResultsCalledFromJasp() && !is.null(data)) { + jaspBase::storeDataSet(data) + } + + optionsWithFormula <- c("binWidthType", "colorPalette", "splitby", "variables") + for (name in optionsWithFormula) { + if ((name %in% optionsWithFormula) && inherits(options[[name]], "formula")) options[[name]] = jaspBase::jaspFormula(options[[name]], data) } + + return(jaspBase::runWrappedAnalysis("jaspTestModule", "AnalysisUpgradeB", "AnalysisUpgrade.qml", options, version, FALSE)) +} \ No newline at end of file diff --git a/R/AnalysisUpgradeIncompatibleAWrapper.R b/R/AnalysisUpgradeIncompatibleAWrapper.R new file mode 100644 index 0000000..79441cf --- /dev/null +++ b/R/AnalysisUpgradeIncompatibleAWrapper.R @@ -0,0 +1,103 @@ +# +# Copyright (C) 2013-2025 University of Amsterdam +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +# This is a generated file. Don't change it! + +#' Analysis A with incompatible to B +#' +#' @param stemAndLeaf, Create a Stem and leaf table. +#' Defaults to \code{FALSE}. +#' @param stemAndLeafScale, The scale parameter controls how much the table is expanded. For example, scale = 2 will cause the table to be roughly twice as long as the default (scale = 1). +AnalysisUpgradeIncompatibleA <- function( + data = NULL, + version = "1", + addSmooth = TRUE, + addSmoothCI = TRUE, + addSmoothCIValue = 0.95, + binWidthType = "sturges", + chartType = "_4histograms", + chartValues = "_2percentages", + colorPalette = "colorblind", + descriptivesIntervalPlot = FALSE, + descriptivesPiechart = FALSE, + descriptivesQQPlot = FALSE, + distPlotDensity = FALSE, + distPlotRug = FALSE, + frequencyTables = FALSE, + frequencyTablesMaximumAmount = 10, + graphTypeAbove = "density", + graphTypeRight = "density", + iqr = FALSE, + kurtosis = FALSE, + mad = FALSE, + madrobust = FALSE, + maximum = TRUE, + mean = TRUE, + median = FALSE, + minimum = TRUE, + mode = FALSE, + numberOfBins = 30, + percentileValuesEqualGroups = FALSE, + percentileValuesEqualGroupsNo = 4, + percentileValuesPercentiles = FALSE, + percentileValuesQuartiles = FALSE, + plotCorrelationMatrix = FALSE, + plotHeight = 320, + plotVariables = FALSE, + plotWidth = 480, + range = FALSE, + regressionType = "smooth", + scatterPlot = FALSE, + shapiro = FALSE, + showLegend = TRUE, + skewness = FALSE, + splitPlotBoxplot = TRUE, + splitPlotColour = FALSE, + splitPlotJitter = FALSE, + splitPlotOutlierLabel = FALSE, + splitPlotViolin = FALSE, + splitPlots = FALSE, + splitby = list(types = list(), value = ""), + standardDeviation = TRUE, + standardErrorMean = FALSE, + statisticsValuesAreGroupMidpoints = FALSE, + stemAndLeaf = FALSE, + stemAndLeafScale = 1, + sum = FALSE, + variables = list(types = list(), value = list()), + variance = FALSE) { + + defaultArgCalls <- formals(jaspTestModule::AnalysisUpgradeIncompatibleA) + defaultArgs <- lapply(defaultArgCalls, eval) + options <- as.list(match.call())[-1L] + options <- lapply(options, eval) + defaults <- setdiff(names(defaultArgs), names(options)) + options[defaults] <- defaultArgs[defaults] + options[["data"]] <- NULL + options[["version"]] <- NULL + + + if (!jaspBase::jaspResultsCalledFromJasp() && !is.null(data)) { + jaspBase::storeDataSet(data) + } + + optionsWithFormula <- c("binWidthType", "colorPalette", "splitby", "variables") + for (name in optionsWithFormula) { + if ((name %in% optionsWithFormula) && inherits(options[[name]], "formula")) options[[name]] = jaspBase::jaspFormula(options[[name]], data) } + + return(jaspBase::runWrappedAnalysis("jaspTestModule", "AnalysisUpgradeIncompatibleA", "AnalysisUpgrade.qml", options, version, FALSE)) +} \ No newline at end of file diff --git a/R/AnalysisUpgradeIncompatibleBWrapper.R b/R/AnalysisUpgradeIncompatibleBWrapper.R new file mode 100644 index 0000000..393802a --- /dev/null +++ b/R/AnalysisUpgradeIncompatibleBWrapper.R @@ -0,0 +1,103 @@ +# +# Copyright (C) 2013-2025 University of Amsterdam +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +# This is a generated file. Don't change it! + +#' Analysis B but then incompatible +#' +#' @param stemAndLeaf, Create a Stem and leaf table. +#' Defaults to \code{FALSE}. +#' @param stemAndLeafScale, The scale parameter controls how much the table is expanded. For example, scale = 2 will cause the table to be roughly twice as long as the default (scale = 1). +AnalysisUpgradeIncompatibleB <- function( + data = NULL, + version = "1", + addSmooth = TRUE, + addSmoothCI = TRUE, + addSmoothCIValue = 0.95, + binWidthType = "sturges", + chartType = "_3pieCharts", + chartValues = "_1frequencies", + colorPalette = "colorblind", + descriptivesIntervalPlot = FALSE, + descriptivesPiechart = FALSE, + descriptivesQQPlot = FALSE, + distPlotDensity = FALSE, + distPlotRug = FALSE, + frequencyTables = FALSE, + frequencyTablesMaximumAmount = 10, + graphTypeAbove = "density", + graphTypeRight = "density", + iqr = FALSE, + kurtosis = FALSE, + mad = FALSE, + madrobust = FALSE, + maximum = TRUE, + mean = TRUE, + median = FALSE, + minimum = TRUE, + mode = FALSE, + numberOfBins = 30, + percentileValuesEqualGroups = FALSE, + percentileValuesEqualGroupsNo = 4, + percentileValuesPercentiles = FALSE, + percentileValuesQuartiles = FALSE, + plotCorrelationMatrix = FALSE, + plotHeight = 320, + plotVariables = FALSE, + plotWidth = 480, + range = FALSE, + regressionType = "smooth", + scatterPlot = FALSE, + shapiro = FALSE, + showLegend = TRUE, + skewness = FALSE, + splitPlotBoxplot = TRUE, + splitPlotColour = FALSE, + splitPlotJitter = FALSE, + splitPlotOutlierLabel = FALSE, + splitPlotViolin = FALSE, + splitPlots = FALSE, + splitby = list(types = list(), value = ""), + standardDeviation = TRUE, + standardErrorMean = FALSE, + statisticsValuesAreGroupMidpoints = FALSE, + stemAndLeaf = FALSE, + stemAndLeafScale = 1, + sum = FALSE, + variables = list(types = list(), value = list()), + variance = FALSE) { + + defaultArgCalls <- formals(jaspTestModule::AnalysisUpgradeIncompatibleB) + defaultArgs <- lapply(defaultArgCalls, eval) + options <- as.list(match.call())[-1L] + options <- lapply(options, eval) + defaults <- setdiff(names(defaultArgs), names(options)) + options[defaults] <- defaultArgs[defaults] + options[["data"]] <- NULL + options[["version"]] <- NULL + + + if (!jaspBase::jaspResultsCalledFromJasp() && !is.null(data)) { + jaspBase::storeDataSet(data) + } + + optionsWithFormula <- c("binWidthType", "colorPalette", "splitby", "variables") + for (name in optionsWithFormula) { + if ((name %in% optionsWithFormula) && inherits(options[[name]], "formula")) options[[name]] = jaspBase::jaspFormula(options[[name]], data) } + + return(jaspBase::runWrappedAnalysis("jaspTestModule", "AnalysisUpgradeIncompatibleB", "AnalysisUpgrade.qml", options, version, FALSE)) +} \ No newline at end of file diff --git a/R/JaspReportTestWrapper.R b/R/JaspReportTestWrapper.R new file mode 100644 index 0000000..98f5b27 --- /dev/null +++ b/R/JaspReportTestWrapper.R @@ -0,0 +1,49 @@ +# +# Copyright (C) 2013-2025 University of Amsterdam +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +# This is a generated file. Don't change it! + +#' Reporting +#' +JaspReportTest <- function( + data = NULL, + version = "1", + plotHeight = 320, + plotWidth = 480, + reportMe = "", + selectedVariable = list(types = list(), value = "")) { + + defaultArgCalls <- formals(jaspTestModule::JaspReportTest) + defaultArgs <- lapply(defaultArgCalls, eval) + options <- as.list(match.call())[-1L] + options <- lapply(options, eval) + defaults <- setdiff(names(defaultArgs), names(options)) + options[defaults] <- defaultArgs[defaults] + options[["data"]] <- NULL + options[["version"]] <- NULL + + + if (!jaspBase::jaspResultsCalledFromJasp() && !is.null(data)) { + jaspBase::storeDataSet(data) + } + + optionsWithFormula <- c("selectedVariable") + for (name in optionsWithFormula) { + if ((name %in% optionsWithFormula) && inherits(options[[name]], "formula")) options[[name]] = jaspBase::jaspFormula(options[[name]], data) } + + return(jaspBase::runWrappedAnalysis("jaspTestModule", "JaspReportTest", "JaspReportTest.qml", options, version, FALSE)) +} \ No newline at end of file diff --git a/R/analysisUpgrade.R b/R/analysisUpgrade.R index e568f2a..ddbaa40 100644 --- a/R/analysisUpgrade.R +++ b/R/analysisUpgrade.R @@ -4,5 +4,5 @@ Analysis <- function(jaspResults, dataset, options) { jaspResults[["optionsOverview"]] <- createJaspHtml(text=paste0(paste0("
  • ", names(options), ": "), options, "
  • ", collapse="\n"), elementType="ul") } -AnalysisUpgradeA <- Analysis; -AnalysisUpgradeB <- Analysis; +AnalysisUpgradeAInternal <- Analysis; +AnalysisUpgradeBInternal <- Analysis; diff --git a/R/customContrastsWrapper.R b/R/customContrastsWrapper.R new file mode 100644 index 0000000..c38cb99 --- /dev/null +++ b/R/customContrastsWrapper.R @@ -0,0 +1,49 @@ +# +# Copyright (C) 2013-2025 University of Amsterdam +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +# This is a generated file. Don't change it! + +#' Custom Contrasts Widget +#' +customContrasts <- function( + data = NULL, + version = "1", + customContrasts = list(), + plotHeight = 320, + plotWidth = 480, + selected = list(types = list(), value = "")) { + + defaultArgCalls <- formals(jaspTestModule::customContrasts) + defaultArgs <- lapply(defaultArgCalls, eval) + options <- as.list(match.call())[-1L] + options <- lapply(options, eval) + defaults <- setdiff(names(defaultArgs), names(options)) + options[defaults] <- defaultArgs[defaults] + options[["data"]] <- NULL + options[["version"]] <- NULL + + + if (!jaspBase::jaspResultsCalledFromJasp() && !is.null(data)) { + jaspBase::storeDataSet(data) + } + + optionsWithFormula <- c("customContrasts", "selected") + for (name in optionsWithFormula) { + if ((name %in% optionsWithFormula) && inherits(options[[name]], "formula")) options[[name]] = jaspBase::jaspFormula(options[[name]], data) } + + return(jaspBase::runWrappedAnalysis("jaspTestModule", "customContrasts", "customContrasts.qml", options, version, FALSE)) +} \ No newline at end of file diff --git a/R/descriptivesLinkWrapper.R b/R/descriptivesLinkWrapper.R new file mode 100644 index 0000000..081de3f --- /dev/null +++ b/R/descriptivesLinkWrapper.R @@ -0,0 +1,93 @@ +# +# Copyright (C) 2013-2025 University of Amsterdam +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +# This is a generated file. Don't change it! + +#' Descriptives! +#' +descriptivesLink <- function( + data = NULL, + version = "1", + addSmooth = TRUE, + addSmoothCI = TRUE, + addSmoothCIValue = 0.95, + chartType = "_4histograms", + chartValues = "_2percentages", + colorPalette = "colorblind", + descriptivesPiechart = FALSE, + descriptivesQQPlot = FALSE, + distPlotDensity = FALSE, + frequencyTables = FALSE, + graphTypeAbove = "density", + graphTypeRight = "density", + iqr = FALSE, + kurtosis = FALSE, + mad = FALSE, + madrobust = FALSE, + maximum = TRUE, + mean = TRUE, + median = FALSE, + minimum = TRUE, + mode = FALSE, + percentileValuesEqualGroups = FALSE, + percentileValuesEqualGroupsNo = 4, + percentileValuesPercentiles = FALSE, + percentileValuesQuartiles = FALSE, + plotCorrelationMatrix = FALSE, + plotHeight = 320, + plotVariables = FALSE, + plotWidth = 480, + range = FALSE, + regressionType = "smooth", + scatterPlot = FALSE, + shapiro = FALSE, + showLegend = TRUE, + skewness = FALSE, + splitPlotBoxplot = TRUE, + splitPlotColour = FALSE, + splitPlotJitter = FALSE, + splitPlotOutlierLabel = FALSE, + splitPlotViolin = FALSE, + splitPlots = FALSE, + splitby = list(types = list(), value = ""), + standardDeviation = TRUE, + standardErrorMean = FALSE, + statisticsValuesAreGroupMidpoints = FALSE, + sum = FALSE, + variables = list(types = list(), value = list()), + variance = FALSE) { + + defaultArgCalls <- formals(jaspTestModule::descriptivesLink) + defaultArgs <- lapply(defaultArgCalls, eval) + options <- as.list(match.call())[-1L] + options <- lapply(options, eval) + defaults <- setdiff(names(defaultArgs), names(options)) + options[defaults] <- defaultArgs[defaults] + options[["data"]] <- NULL + options[["version"]] <- NULL + + + if (!jaspBase::jaspResultsCalledFromJasp() && !is.null(data)) { + jaspBase::storeDataSet(data) + } + + optionsWithFormula <- c("colorPalette", "splitby", "variables") + for (name in optionsWithFormula) { + if ((name %in% optionsWithFormula) && inherits(options[[name]], "formula")) options[[name]] = jaspBase::jaspFormula(options[[name]], data) } + + return(jaspBase::runWrappedAnalysis("jaspTestModule", "descriptivesLink", "CopyOfDescriptives.qml", options, version, FALSE)) +} \ No newline at end of file diff --git a/R/filteredDataEntry.R b/R/filteredDataEntry.R index 90aac0c..a0c821f 100644 --- a/R/filteredDataEntry.R +++ b/R/filteredDataEntry.R @@ -1,7 +1,7 @@ -filteredDataEntry <- function(jaspResults, dataset, options) +filteredDataEntryInternal <- function(jaspResults, dataset, options) { tableOfOptions(jaspResults, options, dataset) -} \ No newline at end of file +} diff --git a/R/infoTestFuncWrapper.R b/R/infoTestFuncWrapper.R new file mode 100644 index 0000000..fbddb5b --- /dev/null +++ b/R/infoTestFuncWrapper.R @@ -0,0 +1,97 @@ +# +# Copyright (C) 2013-2025 University of Amsterdam +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +# This is a generated file. Don't change it! + +#' info testing +#' +#' This is a bunch of info on a form! +#' +#' @param barPlot, Displays the sample means as bars and the confidence intervals or standard errors as error bars for each measure. +#' Defaults to \code{FALSE}. +#' @param barPlotYAxisFixedToZero, Fix horizontal axis to 0: Forces the graph to show the default x-axis at y = 0 +#' Defaults to \code{TRUE}. +#' @param cells, assigned RM +#' @param checkbox, Check this out +#' Defaults to \code{FALSE}. +#' @param doubleFieldWithAfterLabel, this is in doublefield that has a label and an afterLabel +#' @param dropDown, dropdown with info on each value +#' \itemize{ +#' \item \code{"high"} (default) : So High! +#' \item \code{"medium"}: Middling +#' \item \code{"low"}: Down low +#' \item \code{"custom"}: Who knows? +#' } +#' @param dropdownVariableAndExtraValues, You can choose one of the variables or special values +#' @param dropdownVariables, You can choose one of the variables +#' @param integerFieldWithAfterLabel, this is in integerfield that has a label and an afterLabel +#' @param paairs, assigned pairs +#' @param radiobuttongroup, A grouping of buttons of radio +#' \itemize{ +#' \item \code{"rButton2"}: and more info on the other button +#' \item \code{"rButton1"}: and some info on button +#' } +#' @param repeatedMeasuresFactors, FactorLevelList +#' @param simpleTableView, a tableview with some info +#' @param slideOnOuttaHere, slip and slide suicide +#' @param tabview, This is an info field on a tabview +#' @param varA, assigned variables nominal allowed +infoTestFunc <- function( + data = NULL, + version = "1", + barPlot = FALSE, + barPlotCiLevel = 0.95, + barPlotErrorType = "ci", + barPlotYAxisFixedToZero = TRUE, + cells = list(), + checkbox = FALSE, + doubleFieldWithAfterLabel = 0, + dropDown = "high", + dropdownVariableAndExtraValues = list(types = "unknown", value = "one"), + dropdownVariables = list(types = "unknown", value = ""), + dropdownWithVariablesSource = list(types = "unknown", value = ""), + integerFieldWithAfterLabel = 1000, + paairs = list(), + plotHeight = 320, + plotWidth = 480, + radiobuttongroup = "rButton2", + repeatedMeasuresFactors = list(list(levels = list("Level 1", "Level 2"), name = "RM Factor 1")), + simpleTableView = list(), + slideOnOuttaHere = 0.5, + tabview = list(list(marginalMean = FALSE, name = "NEW ITEM", summary = FALSE, syntax = "")), + varA = list(types = list(), value = list())) { + + defaultArgCalls <- formals(jaspTestModule::infoTestFunc) + defaultArgs <- lapply(defaultArgCalls, eval) + options <- as.list(match.call())[-1L] + options <- lapply(options, eval) + defaults <- setdiff(names(defaultArgs), names(options)) + options[defaults] <- defaultArgs[defaults] + options[["data"]] <- NULL + options[["version"]] <- NULL + + + if (!jaspBase::jaspResultsCalledFromJasp() && !is.null(data)) { + jaspBase::storeDataSet(data) + } + + optionsWithFormula <- c("cells", "dropDown", "dropdownVariableAndExtraValues", "dropdownVariables", "dropdownWithVariablesSource", "paairs", "repeatedMeasuresFactors", "simpleTableView", "tabview", "varA") + for (name in optionsWithFormula) { + if ((name %in% optionsWithFormula) && inherits(options[[name]], "formula")) options[[name]] = jaspBase::jaspFormula(options[[name]], data) } + + return(jaspBase::runWrappedAnalysis("jaspTestModule", "infoTestFunc", "TestInfo.qml", options, version, FALSE)) +} \ No newline at end of file diff --git a/R/minMaxFuncWrapper.R b/R/minMaxFuncWrapper.R new file mode 100644 index 0000000..e218460 --- /dev/null +++ b/R/minMaxFuncWrapper.R @@ -0,0 +1,52 @@ +# +# Copyright (C) 2013-2025 University of Amsterdam +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +# This is a generated file. Don't change it! + +#' Min Max +#' +minMaxFunc <- function( + data = NULL, + version = "1", + doubleLower = 0.1, + doubleUpper = 0.5, + integerLower = 1, + integerUpper = 5, + leftFormula = -1, + lowerDefault = 1, + lowerIntegerDefault = 0.1, + plotHeight = 320, + plotWidth = 480, + rightFormula = 1) { + + defaultArgCalls <- formals(jaspTestModule::minMaxFunc) + defaultArgs <- lapply(defaultArgCalls, eval) + options <- as.list(match.call())[-1L] + options <- lapply(options, eval) + defaults <- setdiff(names(defaultArgs), names(options)) + options[defaults] <- defaultArgs[defaults] + options[["data"]] <- NULL + options[["version"]] <- NULL + + + if (!jaspBase::jaspResultsCalledFromJasp() && !is.null(data)) { + jaspBase::storeDataSet(data) + } + + + return(jaspBase::runWrappedAnalysis("jaspTestModule", "minMaxFunc", "MinMaxTest.qml", options, version, FALSE)) +} \ No newline at end of file diff --git a/R/multiDataSet.R b/R/multiDataSet.R new file mode 100644 index 0000000..cc8aff3 --- /dev/null +++ b/R/multiDataSet.R @@ -0,0 +1,195 @@ +# Multi-dataset aware analyses (jasp-desktop branch dataSetAwareVarForm, jaspBase branch +# multiDataSetAware). An analysis whose form declares a dataset selection (dataSetSelectionOption +# on a list/textarea - the Analysis finds out by itself, Description.qml no longer has a +# multiDataSetAware property) gets +# every dataset its options reference as `datasets`. Engine runs AND jaspTools::runAnalysis +# handouts both key that list by SLICE id - the resolved filter id of each queued dataset+filter +# pair (attrs "dataSetIds"/"dataSetNames" map a key back to its dataset) - so both are indexed +# the same way: the form's dataset selection option holds exactly that id, `datasets[[key]]` is +# the whole API, no accessor needed (jaspBase's slice-accessor family, e.g. getSelectionSlice, +# was retired with the zero-export datasets contract). +# Encoding stays PER FILTER: variable options carry names like +# "JASPColumn_D_F__Encoded" (the filter's encoder-view prefix embeds both +# ids) and the dataframes' columns are the very same strings, so a value indexes its column +# directly: slice[[value]]. Two differently filtered versions of one dataset are two slices, +# both available. Result strings are decoded back to the original names on the way to the GUI +# (Engine::sendString and the rbridge callbacks, all consumers of the one global strict decode +# table), so putting encoded names in tables/plots is not just allowed, it is how provenance +# survives: THE PROVENANCE IS IN THE NAME, never in a side channel. While such +# a run is active, readDataSet* stops: one 'current dataset' is meaningless here. +# +# Options contract (the Phase-3 flip): `options$.meta` is GENERATED at send +# time and carries no dataSetId/filterId; the transient `.filterIds` sidecar is stripped at the +# engine boundary before R sees anything. metaDump() below echoes both facts as the +# PROVENANCE_AUDIT marker the RPC verifier asserts on every run. +# +# Qualified column references: a TERM option may +# arrive with qualified surface forms ("Dataset"."Filter"."Column"("Type"), minimal per +# context); the C++ appliers parse them into typed terms with filterIds before the run, so R +# receives them as ordinary encoded names - no R-side grammar. A STRING option (plain +# text input) receives the qualified form verbatim - the v1 stance: it passes through and +# modules slice on it or not, at their choice; module code that needs to interpret one uses +# the echo pattern of multiDataSetControlsInternal (report what was received, then resolve on +# demand) and never re-implements the token grammar. +# +# The two aware analyses here: multiDataSetFuncInternal (form testMultiDataSet.qml - two +# per-list dataset selection options dataSetA/dataSetB, the wrapper fixture) and +# multiDataSetControlsInternal (the component zoo testMultiDataSetControls.qml, +# assigned lists, row controls with same-name twins, interactions, factor levels +# via encodeThis, a textarea host, a cross-host typed bindTo copy - each reports what it +# received and where it resolved). + +multiDataSetFuncInternal <- function(jaspResults, dataset, options, datasets = NULL) { + aware <- !is.null(datasets) + + jaspResults[["explanation"]] <- createJaspHtml(paste0( + if (aware) + paste0("Aware run. This analysis received ", length(datasets), + " slice(s) as the `datasets` argument; readDataSet() is disabled while it runs.") + else + "Non-aware run. No `datasets` argument: data comes in through `dataset`/readDataSet as always, and the dataset selection in the form should have been inert.", + "

    Use two datasets in the workspace (File - New data set or add another CSV) and drag variables from both into the variables form.")) + + dataSetOverviewTable(jaspResults, datasets, dataset) + selectedVariablesTable(jaspResults, options, datasets, dataset) + + if (isTRUE(options$showMeta)) + metaDump(jaspResults, options) + + if (isTRUE(options$guardDemo)) + readDataSetGuardDemo(jaspResults) +} + +dataSetOverviewTable <- function(jaspResults, datasets, dataset) { + table <- createJaspTable("Datasets in this run") + jaspResults[["dataSetOverview"]] <- table + + table$addColumnInfo("id", title = "Dataset id") + table$addColumnInfo("title", title = "Title") + table$addColumnInfo("rows", title = "Rows") + table$addColumnInfo("cols", title = "Columns") + table$addColumnInfo("columnNames", title = "Column names (decoded on the way out)") + + rows <- data.frame(id = character(), title = character(), rows = integer(), + cols = integer(), columnNames = character(), stringsAsFactors = FALSE) + + if (!is.null(datasets)) { + for (key in names(datasets)) { + df <- datasets[[key]] + rows <- rbind(rows, data.frame( + id = as.character(attr(datasets, "dataSetIds")[[key]] %||% key), + title = as.character(attr(datasets, "dataSetNames")[[key]] %||% key), + rows = nrow(df), cols = ncol(df), + columnNames = paste(names(df), collapse = ", "), + stringsAsFactors = FALSE)) + } + } else if (!is.null(dataset)) { + rows <- data.frame(id = "(current)", title = "(the one dataset)", rows = nrow(dataset), + cols = ncol(dataset), columnNames = paste(names(dataset), collapse = ", "), + stringsAsFactors = FALSE) + } + + table$setData(rows) +} + +selectedVariablesTable <- function(jaspResults, options, datasets, dataset) { + table <- createJaspTable("Selected variables") + jaspResults[["selectedVariables"]] <- table + + table$addColumnInfo("source", title = "Form") + table$addColumnInfo("variable", title = "Variable (decoded on the way out)") + table$addColumnInfo("dataSetId", title = "Dataset id") + table$addColumnInfo("dataSetName", title = "Dataset title") + table$addColumnInfo("n", title = "n") + table$addColumnInfo("values", title = "mean (scale) / first levels (factor)") + + rows <- data.frame(source = character(), variable = character(), dataSetId = character(), + dataSetName = character(), n = integer(), values = character(), + stringsAsFactors = FALSE) + + for (entry in list(list(form = "A", sel = "dataSetA", values = .dependentValues(options$dependentA)), + list(form = "B", sel = "dataSetB", values = .dependentValues(options$dependentB)))) { + + for (value in entry$values) { + # The contract, no accessors: datasets is keyed by FILTER id and the form's dataset + # selection option holds exactly that id, so the option IS the key - two forms on one + # dataset with different filters index different slices. attrs dataSetIds/dataSetNames + # map a key to its dataset (titles are display-only: user-editable, never a key). + key <- if (is.null(datasets)) NULL else { + sel <- trimws(as.character(utils::tail(unlist(options[[entry$sel]]), 1L))) + if (is.na(sel) || !nzchar(sel) || !sel %in% names(datasets)) NULL else sel + } + slice <- if (is.null(datasets)) dataset else if (!is.null(key)) datasets[[key]] else NULL + + columnData <- if (!is.null(slice) && value %in% names(slice)) slice[[value]] else NULL + + row <- list( + source = entry$form, + variable = value, + dataSetId = if (is.null(key)) "-" else as.character(attr(datasets, "dataSetIds")[[key]]), + dataSetName = if (is.null(key)) "-" else as.character(attr(datasets, "dataSetNames")[[key]]), + n = NA_integer_, values = "") + + if (!is.null(columnData)) + row <- .fillDescription(row, columnData) + + rows <- rbind(rows, data.frame(row, stringsAsFactors = FALSE, check.names = FALSE)) + } + } + + table$setData(rows) +} + +metaDump <- function(jaspResults, options) { + meta <- options[[".meta"]] + metaText <- jsonlite::toJSON(if (is.null(meta)) list() else meta, auto_unbox = TRUE, pretty = 2) + # R must never see provenance - the transient `.filterIds` sidecar is stripped + # at the engine boundary and the send-time `.meta` carries no dataSetId/filterId. + # The audit markers echo exactly what arrived so the RPC verifier can assert the contract. + provenanceKeys <- sum(lengths(regmatches(metaText, gregexpr("dataSetId|filterId", metaText)))) + filterIdsSeen <- !is.null(options[[".filterIds"]]) + jaspResults[["metaDump"]] <- createJaspHtml(paste0( + "PROVENANCE_AUDIT filterIdsSeen=", if (filterIdsSeen) "true" else "false", + " metaProvenanceKeys=", provenanceKeys, "
    ", + "options$.meta
    ", metaText, "
    ")) +} + +readDataSetGuardDemo <- function(jaspResults) { + message <- tryCatch( + { + .readDataSetToEnd(columns = "MPG") # would work outside an aware run + "No error - readDataSet() should have stopped though! (not in multi-dataset mode?)" + }, + error = function(e) paste0("readDataSet() stopped it, as it should:
    ", + "
    ", conditionMessage(e), "
    ")) + + jaspResults[["guardDemo"]] <- createJaspHtml(message) +} + +# The non-aware counterpart (multiDataSetNonAwareInternal + Description entry) was removed by +# every other analysis here is already a plain single-dataset one, and the +# twin's generated wrapper had a stale qml pointer so its R entry point always errored. The +# "Non-aware run." branch above stays: it renders whenever `datasets` never arrives (old +# jaspBase, direct Internal calls). + +# ------------------------------------------------------------------ internals + +`%||%` <- function(x, y) if (is.null(x)) y else x + +.dependentValues <- function(values) { + if (is.null(values)) return(character(0)) + values <- unlist(values, recursive = TRUE) + as.character(values[!is.na(values) & as.character(values) != ""]) +} + +.fillDescription <- function(entry, columnData) { + if (is.numeric(columnData)) { + entry$n <- sum(!is.na(columnData)) + entry$values <- sprintf("mean %.2f, sd %.2f", mean(columnData, na.rm = TRUE), sd(columnData, na.rm = TRUE)) + } else { + levels <- unique(as.character(stats::na.omit(columnData))) + entry$n <- length(stats::na.omit(columnData)) + entry$values <- paste(utils::head(levels, 5), collapse = ", ") + } + entry +} diff --git a/R/multiDataSetControls.R b/R/multiDataSetControls.R new file mode 100644 index 0000000..b022190 --- /dev/null +++ b/R/multiDataSetControls.R @@ -0,0 +1,774 @@ +# the component-zoo echo. Receives the generated options + the per-slice +# `datasets` (contract: keyed by SLICE/filter id, attrs dataSetIds/dataSetNames map a key back +# to its dataset - see multiDataSet.R for the full contract notes) and reports, per control +# family, EVERY value exactly as received: the encoded name (results are decoded on the way out, +# which is itself part of what we verify), which slice index it resolved in, and the row +# children. Tools/verifyMultiDataSetRpc.py asserts all of it. Cheap by design: no statistics +# beyond a mean/level-list per resolved column. +# +# Families (see inst/qml/testMultiDataSetControls.qml): +# assigned-list Zoo A - plain assigned list under a selecting available list +# combobox-host Zoo A - DropDown served by the same host: value stays a PLAIN STRING +# components-rows Zoo C - row-bearing variables list (the TableFields shape; shared +# RowControls/TermRowKey machinery), sourced from a +# default-provider pool (every assigned list has >= 1 source - +# owner rule; the bare standalone list died with Zoo B): +# per-row term + keep + shift; same-name twin rows from two +# filters keep their OWN children (the TermRowKey +# re-keying); the pool's key is -1, so the stamped twins are +# never swept by any reset +# pool-hostless Zoo M - the default-provider pool's companion (no host selection, +# participation key -1): sent-BARE terms resolve through the +# analysis-level fallback (the use-time resolution), +# sent-stamped terms keep their own stamp +# pool-f3 Zoo M - the EXTRA SOURCE's companion: the pool hosted by the THIRD +# filter of the same test dataset (f3-origin terms carry its +# stamp; the anchor scenario's namespace) +# derived-pool Zoo N - the derived pool's companion: refs from assignedA + +# assignedDefault (mixed provenance), each keeping its own stamp +# interaction-list Zoo D - combination terms: all components share the term filterId +# factor-levels Zoo E - encodeThis -> extras roundtrip (names never slice columns; +# they must come back decoded, no raw JaspExtraOptions_ leakage) +# textarea-host Zoo F - R-code textarea: used columns encoded through its own host +# bindto-cross-host Zoo G - typed copy bound across differently-selected hosts: keeps the +# SOURCE filterId, encodes through the SOURCE's view +# dropdown-dependent Zoo I - dependent dropdown: option LIST driven by assignedA's value; its +# value encodes through the SOURCE's host namespace (a dropdown +# with a source inherits the source's selection host) +# pairs-list Zoo J - AssignedPairsVariablesList: its 2 columns are PAIRED into +# 2-component terms (bound value [["Score","Age"]]); per-pair +# resolution + correlation +# factors-form Zoo K - FactorsForm (the FactorsList family): per factor name/title + +# terms. The form's inner panels are not hosts, but the per-factor +# sidecar (FactorsFormBase::createFilterIds) now +# transports each term's OWN filterId per factor and per term: a +# factor term carries the stamp of the host its source list synced +# it from (here assignedK -> filter 1) and encodes through THAT +# filter's view - resolved slice by slice, no analysis-level +# fallback (the hostless-panel deferral is CLOSED; pinned +# here, not faked). +# REGRESSION PINNED C++-SIDE: a factor term whose +# column the resolved namespace lacks (e.g. a NOMINAL Group term +# while the shown dataset is DataSet 2) used to FATAL the whole +# run - rbridge_readDataSetRequested refused "Group.nominal" +# ("not a columnname at all") because the collector inserted +# explicitly-typed terms without an existence check (pre-existing +# gap, reachable only since the aware stamping). The collector now +# refuses to queue columns the resolved namespace does not have +# (Common/columnencoder.cpp); with the per-factor sidecar the term +# resolves in its OWN namespace (where the column exists), so the +# phantom-namespace route is gone entirely - the zoo pins the +# resolved row + clean audit instead of the old honest unresolved +# row. +# tableview Zoo L - bound TableView (row count driven by Zoo H's integer); cells +# are plain values (no provenance - not a Terms option) and are +# USED in the cross-slice computation, not just echoed +# scalar plain values, no provenance anywhere +# +# RESULTS INVENTORY (the point of the zoo: data and flows must be VISIBLE, "show the +# loaded data"): +# dataSetOverview one row per slice (unchanged) +# dataPreview_ THE LOADED DATA: per slice, every column's name AND first `rowsH` VALUES +# (head of data per slice) - proves the qualified-name -> decoded-token -> +# correct-slice chain end to end, visible in the results panel. Row count +# driven by Zoo H's integer field (rowsH). +# controlsProvenance per family: every received value, its row children, resolved slice +# scalarEchoes scalar families as received + WHERE each is used +# optionFlows the flow demonstrations: dependent dropdown's effective selection, +# checkbox gate state, row-count effect, radio statistic +# crossSlice computations ACROSS slices/lists of different provenance: interaction +# product, cross-host column product, pair correlation, tableL multipliers +# applied per slice, scale factor, radio-driven statistic +# encodeDemo the encodeThis/FactorLevelList transformation APPLIED: factor/level +# names from Zoo E used to recode an actual column +# gatedResult the checkbox-gated sub-result: created ONLY when gateH is on (a gated +# result must be absent, not an empty table) +# metaDump the PROVENANCE_AUDIT (unchanged) +# +# Where a family cannot produce a computation for a principled reason it says so in its row +# (sliderH: a bare fraction has no sensible slice computation - echo only; tableL cells: plain +# values by design) - documented, not faked. + +multiDataSetControlsFuncInternal <- function(jaspResults, dataset, options, datasets = NULL) { + aware <- !is.null(datasets) + + jaspResults[["explanation"]] <- createJaspHtml(paste0( + if (aware) + paste0("Aware run. Component zoo received ", length(datasets), + " slice(s) as `datasets`; every family below echoes what it received, the loaded ", + "data is shown per slice (first ", .intOption(options$rowsH, 5L), " rows) and the ", + "option flows are demonstrated with real computations.") + else + "Non-aware run. No `datasets` argument: this analysis must run multiDataSetAware for the routing tables to mean anything.", + "
    See the check list at the top of Tools/verifyMultiDataSetRpc.py's zoo section for what each family proves.")) + + dataSetOverviewTable(jaspResults, datasets, dataset) + dataPreviewTables(jaspResults, datasets, options) + controlsProvenanceTable(jaspResults, options, datasets) + scalarEchoTable(jaspResults, options) + flowsTable(jaspResults, options, datasets) + crossSliceTable(jaspResults, options, datasets) + encodeDemoTable(jaspResults, options, datasets) + + # the checkbox-gated sub-result: a GATE, so the object must not exist while closed + if (isTRUE(options$gateH)) + gatedResultTable(jaspResults, options, datasets) + + if (isTRUE(options$showMeta)) + metaDump(jaspResults, options) +} + +# ------------------------------------------------------------------ resolution helpers + +.resolveSlice <- function(token, datasets) { + # an aware option value IS the encoded column name of exactly one slice: index it + if (is.null(datasets) || !is.character(token) || length(token) != 1 || !nzchar(token)) + return(NULL) + hits <- names(datasets)[vapply(names(datasets), + function(k) token %in% names(datasets[[k]]), logical(1))] + if (length(hits) == 0) NULL else hits[[1]] +} + +.sliceMeta <- function(key, datasets) { + if (is.null(key)) return(list(id = "-", name = "-")) + dsId <- attr(datasets, "dataSetIds")[[key]] + dsName <- attr(datasets, "dataSetNames")[[key]] + list(id = if (is.null(dsId)) as.character(key) else as.character(dsId), + name = if (is.null(dsName)) as.character(key) else as.character(dsName)) +} + +.describeValue <- function(key, token, datasets) { + if (is.null(key)) return(list(n = NA_integer_, values = "")) + .fillDescription(list(n = NA_integer_, values = ""), datasets[[key]][[token]]) +} + +# scalar option readers: scalars are never provenanced, so they arrive as plain values +.intOption <- function(x, default = NA_integer_) { + if (is.null(x) || length(x) != 1 || is.na(x)) return(default) + v <- suppressWarnings(as.integer(x)) + if (is.na(v)) default else v +} +.numOption <- function(x, default = NA_real_) { + if (is.null(x) || length(x) != 1 || is.na(x)) return(default) + v <- suppressWarnings(as.numeric(x)) + if (is.na(v)) default else v +} +.strOption <- function(x, default = "") { + if (is.null(x) || length(x) != 1 || is.na(x)) return(default) + x <- trimws(as.character(x)) + if (!nzchar(x) || identical(x, "0")) default else x # '0' is the empty combobox sentinel +} + +# the first scale column of a slice (for computations), with its provenance label +.firstScaleColumn <- function(datasets, keys = names(datasets)) { + for (key in keys) { + df <- datasets[[key]] + for (cn in names(df)) { + col <- df[[cn]] + if (is.numeric(col) && sum(!is.na(col)) > 1) + return(list(key = key, column = cn, values = as.numeric(col))) + } + } + NULL +} + +.statLabel <- function(stat, values) { + if (is.null(values) || !length(values)) return("n/a") + if (identical(stat, "sum")) sprintf("sum %.2f", sum(values, na.rm = TRUE)) + else sprintf("mean %.2f", mean(values, na.rm = TRUE)) +} + +# ------------------------------------------------------------------ THE LOADED DATA (a) +# One table per slice: every column's name (decoded via jaspBase::decodeName - the engine keeps +# the same deterministic strict table as the desktop) and its first `rowsH` VALUES - the whole +# qualified-name -> decoded-token -> correct-slice chain, visible. Field names are passed DECODED +# (a non-token passes through the strict table untouched), with a " (k)" suffix when one slice +# holds several type-variant tokens for the same column (e.g. the same column encoded once as +# scale and once as ordinal) so the schema never collides. +dataPreviewTables <- function(jaspResults, datasets, options) { + if (is.null(datasets)) return(invisible()) + maxRows <- max(.intOption(options$rowsH, 5L), 1L) + + for (key in names(datasets)) { + df <- datasets[[key]] + meta <- .sliceMeta(key, datasets) + table <- createJaspTable(paste0("Loaded data - ", meta$name, " (slice ", key, ", ", + nrow(df), " rows), first ", min(maxRows, nrow(df)), " rows")) + jaspResults[[paste0("dataPreview_", key)]] <- table + + table$addColumnInfo("row", title = "row #") + seen <- character() + fields <- character() + for (cn in names(df)) { + label <- tryCatch(jaspBase::decodeName(cn), error = function(e) cn) + if (label %in% seen) label <- paste0(label, " (", sum(seen == label) + 1, ")") + seen <- c(seen, label) + fields[[cn]] <- label + table$addColumnInfo(label, title = label) + } + + n <- min(maxRows, nrow(df)) + rows <- data.frame(row = seq_len(n)) + for (cn in names(df)) { + col <- df[[cn]] + if (is.factor(col)) col <- as.character(col) + rows[[fields[[cn]]]] <- utils::head(col, n) + } + table$setData(rows) + } +} + +# ------------------------------------------------------------------ the echo table + +controlsProvenanceTable <- function(jaspResults, options, datasets) { + table <- createJaspTable("Controls provenance") + jaspResults[["controlsProvenance"]] <- table + + table$addColumnInfo("family", title = "Family") + table$addColumnInfo("option", title = "Option") + table$addColumnInfo("received", title = "Received value (decoded on the way out)") + table$addColumnInfo("rowChild", title = "Row children") + table$addColumnInfo("slice", title = "Resolved slice (filter id)") + table$addColumnInfo("dataSetId", title = "Dataset id") + table$addColumnInfo("n", title = "n") + table$addColumnInfo("values", title = "mean (scale) / levels (factor)") + + rows <- data.frame(family = character(), option = character(), received = character(), + rowChild = character(), slice = character(), dataSetId = character(), + n = integer(), values = character(), stringsAsFactors = FALSE) + + addRow <- function(df, family, option, received, rowChild = "", key = NULL, + n = NA_integer_, values = "") { + meta <- .sliceMeta(key, datasets) + rbind(df, data.frame(family = family, option = option, received = as.character(received), + rowChild = as.character(rowChild), + slice = if (is.null(key)) "-" else as.character(key), + dataSetId = meta$id, n = n, values = as.character(values), + stringsAsFactors = FALSE, check.names = FALSE)) + } + + # --- plain term lists: Zoo A / Zoo G / Zoo K's source / Zoo M / Zoo N --------------------------- + for (entry in list( + list(fam = "assigned-list", opt = "assignedA", vals = .dependentValues(options$assignedA)), + list(fam = "factors-source", opt = "assignedK", vals = .dependentValues(options$assignedK)), + list(fam = "bindto-cross-host", opt = "boundG", vals = .dependentValues(options$boundG)), + list(fam = "pool-hostless", opt = "assignedDefault", vals = .dependentValues(options$assignedDefault)), + list(fam = "pool-f3", opt = "assignedThree", vals = .dependentValues(options$assignedThree)), + list(fam = "derived-pool", opt = "assignedDerived", vals = .dependentValues(options$assignedDerived)))) { + for (value in entry$vals) { + key <- .resolveSlice(value, datasets) + desc <- .describeValue(key, value, datasets) + rows <- addRow(rows, entry$fam, entry$opt, value, key = key, n = desc$n, values = desc$values) + } + } + + # --- dependent dropdown (Zoo I): plain string value whose OPTION LIST is driven by assignedA ---- + comboI <- .strOption(options$comboI, default = "") + if (nzchar(comboI)) { + key <- .resolveSlice(comboI, datasets) + desc <- .describeValue(key, comboI, datasets) + rows <- addRow(rows, "dropdown-dependent", "comboI", comboI, key = key, n = desc$n, values = desc$values) + } + + # --- paired list (Zoo J): AssignedPairsVariablesList pairs its 2 columns into 2-component + # terms (bound value [["Score","Age"]]) - one row per PAIR, both components resolved, and a + # real computation per pair (correlation of the two paired columns in their shared slice). + pairsJ <- options$pairsJ + if (!is.null(pairsJ) && is.list(pairsJ)) { + pairRows <- if (!is.null(pairsJ$value)) pairsJ$value else pairsJ + pairIdx <- 0 + for (row in pairRows) { + comps <- as.character(unlist(row)) + if (length(comps) == 0) next + pairIdx <- pairIdx + 1 + keys <- vapply(comps, function(c) { k <- .resolveSlice(c, datasets); if (is.null(k)) "-" else k }, character(1)) + joint <- if (length(unique(keys)) == 1 && keys[[1]] != "-") keys[[1]] else NULL + child <- paste0("pair ", pairIdx, ": ", + paste(comps, "(", keys, ")", sep = "", collapse = " + ")) + corTxt <- "" + if (!is.null(joint) && length(comps) == 2) { + c1 <- datasets[[joint]][[comps[[1]]]]; c2 <- datasets[[joint]][[comps[[2]]]] + if (is.numeric(c1) && is.numeric(c2) && sum(complete.cases(c1, c2)) > 2) + corTxt <- sprintf("r = %.3f", stats::cor(c1, c2, use = "complete.obs")) + } + rows <- addRow(rows, "pairs-list", "pairsJ", paste(comps, collapse = " + "), + rowChild = child, key = joint, values = if (nzchar(corTxt)) corTxt else + paste0("slices: ", paste(unique(keys), collapse = "/"))) + } + } + + # --- factors form (Zoo K): per factor name + title + its terms' slices --------------------------- + factorsK <- options$factorsK + if (!is.null(factorsK) && is.list(factorsK)) { + for (f in factorsK) { + if (!is.list(f) || is.null(f$name)) next + vars <- as.character(unlist(f$variables %||% f$indicators %||% f$components %||% list())) + keys <- vapply(vars, function(v) { k <- .resolveSlice(v, datasets); if (is.null(k)) "-" else k }, character(1)) + child <- paste0("title=", if (is.null(f$title)) "-" else f$title, + "; vars: ", if (length(vars)) paste(vars, "(", keys, ")", sep = "", collapse = ", ") else "(none)") + rows <- addRow(rows, "factors-form", "factorsK", f$name, rowChild = child, + key = if (length(unique(keys)) == 1 && keys[[1]] != "-") keys[[1]] else NULL, + values = if (length(vars)) + paste0(length(vars), " var(s), slices: ", paste(unique(keys), collapse = "/")) + else "empty factor") + } + } + + # --- bound table view (Zoo L): plain cells (not a Terms option - no provenance by design) -------- + tableL <- .tableViewOption(options$tableL) + if (!is.null(tableL)) { + rows <- addRow(rows, "tableview", "tableL", + paste0(nrow(tableL$values), " rows x ", ncol(tableL$values), " cols"), + rowChild = paste0("cells: ", paste(utils::head(as.character(tableL$values), 6), collapse = ", "), + if (nrow(tableL$values) * ncol(tableL$values) > 6) ", ..." else ""), + values = "plain cells (no provenance: a tableview option is not a Terms option)") + } + + # --- combobox served by Zoo A's host: PLAIN string value (never term-objectified) --------------- + combo <- trimws(as.character(utils::tail(unlist(options$comboA), 1L))) + if (!is.na(combo) && nzchar(combo) && !identical(combo, "0")) { + key <- .resolveSlice(combo, datasets) + desc <- .describeValue(key, combo, datasets) + rows <- addRow(rows, "combobox-host", "comboA", combo, key = key, n = desc$n, values = desc$values) + } + + # --- ComponentsList rows (Zoo C): per-row term + its OWN row children ---------------------------- + rowsC <- options$rowsC + if (!is.null(rowsC) && is.list(rowsC) && !is.null(rowsC$value) && is.list(rowsC$value)) + rowsC <- rowsC$value # tolerate a {value, ...} wrapper + if (!is.null(rowsC) && is.list(rowsC)) { + for (row in rowsC) { + if (!is.list(row)) next + tokens <- as.character(unlist(row$variable)) + token <- if (length(tokens) > 0) tokens[[1]] else "" + if (!nzchar(token)) next + key <- .resolveSlice(token, datasets) + desc <- .describeValue(key, token, datasets) + child <- paste0("keep=", if (is.null(row$keep)) "NA" else as.logical(row$keep), + ", shift=", if (is.null(row$shift)) "NA" else as.character(row$shift)) + rows <- addRow(rows, "components-rows", "rowsC", token, rowChild = child, + key = key, n = desc$n, values = desc$values) + } + } + + # --- interaction terms (Zoo D): components of one row share the term filterId -------------------- + # jaspBase hands {value,types} options UNWRAPPED to the analysis (the value list directly, + # types as the sibling optionX.types), so accept both shapes. + inter <- options$interactionD + if (!is.null(inter) && is.list(inter)) { + interRows <- if (!is.null(inter$value)) inter$value else inter + for (row in interRows) { + comps <- as.character(unlist(row$components)) + if (length(comps) == 0) next + keys <- unique(vapply(comps, function(c) { + k <- .resolveSlice(c, datasets); if (is.null(k)) "-" else k + }, character(1))) + joint <- if (length(keys) == 1 && keys[[1]] != "-") keys[[1]] else NULL + rows <- addRow(rows, "interaction-list", "interactionD", + paste(comps, collapse = " : "), rowChild = paste0("order=", length(comps)), + key = joint, + n = if (is.null(joint)) NA_integer_ else + sum(!is.na(datasets[[joint]][[comps[[1]]]])), + values = if (is.null(joint)) paste("slices:", paste(keys, collapse = "/")) else + paste0("components share slice ", joint)) + } + } + + # --- factor levels (Zoo E): extras-encoded names, NOT slice columns ------------------------------ + factors <- options$factorsE + if (!is.null(factors) && is.list(factors)) { + for (f in factors) { + if (!is.list(f) || is.null(f$name)) next + levels <- as.character(unlist(f$levels)) + rows <- addRow(rows, "factor-levels", "factorsE", f$name, + rowChild = paste(levels, collapse = ", "), + values = paste0(length(levels), " level(s), extras roundtrip")) + } + } + + # --- textarea (Zoo F): tokens encoded through the textarea's own host ---------------------------- + code <- options$codeF + if (!is.null(code) && is.list(code)) { + used <- unique(as.character(unlist(code$columns))) + # the received model text itself (encoded at bind time through the host view; decoded again + # on the way out - the verifier asserts it round-trips to the original plain text) + if (is.character(code$model) && nzchar(code$model)) + rows <- addRow(rows, "textarea-host", "codeF(text)", code$model, + rowChild = "model as received (decoded on the way out)") + for (token in used) { + if (!nzchar(token)) next + key <- .resolveSlice(token, datasets) + desc <- .describeValue(key, token, datasets) + rows <- addRow(rows, "textarea-host", "codeF", token, rowChild = "detected column", + key = key, n = desc$n, values = desc$values) + } + model <- code$model + if (is.character(model) && nzchar(model)) { + # every column of every slice mentioned in the (encoded) model text resolves to that slice; + # a token encoded through the WRONG namespace would not be found in the selected slice at all + mentioned <- character() + for (key in names(datasets)) + for (column in names(datasets[[key]])) + if (grepl(column, model, fixed = TRUE)) mentioned <- c(mentioned, paste0(key, "|", column)) + if (length(mentioned) > 0) + for (m in mentioned) { + parts <- strsplit(m, "|", fixed = TRUE)[[1]] + key <- parts[[1]]; column <- parts[[2]] + desc <- .describeValue(key, column, datasets) + rows <- addRow(rows, "textarea-host", "codeF(model)", column, + rowChild = "found in model text", key = key, n = desc$n, values = desc$values) + } + else + rows <- addRow(rows, "textarea-host", "codeF(model)", "-", + rowChild = "no slice column mentioned in model text") + } + } + + # --- scalars: plain values, no provenance anywhere ------------------------------------------ + rows <- addRow(rows, "scalar", "plainSwitch", as.character(isTRUE(options$plainSwitch)), + rowChild = "no provenance (scalars unstamped)") + rows <- addRow(rows, "combobox-plain", "comboG", as.character(options$comboG), + rowChild = "plain string (combos never term-objectified)") + + table$setData(rows) +} + +# ------------------------------------------------------------------ scalar echoes (b) +# Every scalar family AS RECEIVED plus where it is USED - the "echo" half of the scalar contract; +# the "use" half lives in the flow/cross-slice tables. +scalarEchoTable <- function(jaspResults, options) { + table <- createJaspTable("Scalar & simple control echoes") + jaspResults[["scalarEchoes"]] <- table + + table$addColumnInfo("option", title = "Option") + table$addColumnInfo("family", title = "Family") + table$addColumnInfo("received", title = "Received value (as received)") + table$addColumnInfo("usedIn", title = "Used in") + + gated <- if (isTRUE(options$gateH)) "gate OPEN" else "gate closed (received, but control disabled in the form)" + + rows <- data.frame( + option = c("textH", "rowsH", "doubleH", "sliderH", "radioH", "gateH", "gatedH"), + family = c("text (BoundControlTextBox)", "integer (BoundControlNumberField)", + "real (BoundControlNumberField)", "slider", "radio (RadioButtonGroup)", + "checkbox (BoundControlCheckBox)", "real, checkbox-gated"), + received = c(.strOption(options$textH), as.character(.intOption(options$rowsH)), + as.character(.numOption(options$doubleH)), as.character(.numOption(options$sliderH)), + as.character(.strOption(options$radioH)), as.character(isTRUE(options$gateH)), + paste0(as.character(.numOption(options$gatedH)), " (", gated, ")")), + usedIn = c("echoed verbatim (no computation has a use for free text)", + "data-preview row count + Zoo L row count", + "cross-slice scale factor", + "echo only by design: a bare fraction has no sensible slice computation (documented, not faked)", + "flow statistic (cross-slice table)", + "gates the gated sub-result", + "gated sub-result threshold"), + stringsAsFactors = FALSE) + + table$setData(rows) +} + +# ------------------------------------------------------------------ flow demonstrations (c) +flowsTable <- function(jaspResults, options, datasets) { + table <- createJaspTable("Option flows") + jaspResults[["optionFlows"]] <- table + + table$addColumnInfo("flow", title = "Flow") + table$addColumnInfo("trigger", title = "Trigger (control)") + table$addColumnInfo("effect", title = "Effect") + table$addColumnInfo("observed", title = "Observed in this run") + + # 1. dependent dropdown: assignedA's content drives comboI's option list + assignedA <- .dependentValues(options$assignedA) + comboI <- .strOption(options$comboI, "") + comboIKey <- .resolveSlice(comboI, datasets) + rows <- data.frame( + flow = "dependent dropdown", + trigger = "assignedA (Zoo A's list)", + effect = "comboI's option list = assignedA's terms; its effective selection follows", + observed = paste0("source list: [", paste(assignedA, collapse = ", "), "]; selection: ", + if (nzchar(comboI)) paste0("\"", comboI, "\" -> slice ", + if (is.null(comboIKey)) "?" else comboIKey) else "(empty)"), + stringsAsFactors = FALSE) + + # 2. checkbox gate: gateH toggles the existence of the gated sub-result + rows <- rbind(rows, data.frame( + flow = "checkbox gate", + trigger = "gateH (Zoo H checkbox)", + effect = "gated sub-result exists only while the gate is open; gatedH is its threshold", + observed = paste0("gate ", + if (isTRUE(options$gateH)) + paste0("OPEN -> 'Gated sub-result' table produced (threshold ", .numOption(options$gatedH), ")") + else + "closed -> no gated sub-result"), + stringsAsFactors = FALSE)) + + # 3. number field: rowsH controls the shown row count (previews + Zoo L) + tableL <- .tableViewOption(options$tableL) + previewRows <- if (is.null(datasets)) 0 else + min(.intOption(options$rowsH, 5L), min(vapply(datasets, nrow, integer(1)))) + rows <- rbind(rows, data.frame( + flow = "number field -> shown rows", + trigger = "rowsH (Zoo H integer)", + effect = "row count of every 'Loaded data' preview and of the Zoo L table view", + observed = paste0("rowsH=", .intOption(options$rowsH), "; previews show ", previewRows, + " rows; tableL has ", + if (is.null(tableL)) "?" else nrow(tableL$values), " rows"), + stringsAsFactors = FALSE)) + + # 4. radio: picks the statistic used by the cross-slice computation + statCol <- .columnByToken(.strOption(options$comboA, ""), datasets) + rows <- rbind(rows, data.frame( + flow = "radio -> statistic", + trigger = "radioH (Zoo H radio group)", + effect = "the 'radio-driven statistic' row of the cross-slice table switches mean/sum", + observed = .statLabel(.strOption(options$radioH, "mean"), + if (is.null(statCol)) NULL else statCol$values), + stringsAsFactors = FALSE)) + + # 5. variables list -> interaction terms: the terms (and their computation) follow the list + inter <- options$interactionD + nTerms <- 0L + if (!is.null(inter) && is.list(inter)) { + interRows <- if (!is.null(inter$value)) inter$value else inter + nTerms <- length(interRows) + } + rows <- rbind(rows, data.frame( + flow = "variables list -> interaction terms", + trigger = "interactionD (Zoo D list)", + effect = "the 'interaction product' rows of the cross-slice table follow the list's terms", + observed = paste0(nTerms, " term(s) received"), + stringsAsFactors = FALSE)) + + table$setData(rows) +} + +# ------------------------------------------------------------------ cross-slice computations (c) +# The results-side point of provenance: computations that COMBINE values of different slices / +# lists, each row carrying its inputs' provenance as dataset titles + slice keys (never tokens). +crossSliceTable <- function(jaspResults, options, datasets) { + table <- createJaspTable("Cross-slice computations") + jaspResults[["crossSlice"]] <- table + + table$addColumnInfo("computation", title = "Computation") + table$addColumnInfo("inputs", title = "Inputs (with provenance)") + table$addColumnInfo("value", title = "Value") + + rows <- data.frame(computation = character(), inputs = character(), value = character(), + stringsAsFactors = FALSE) + addRow2 <- function(computation, inputs, value) { + rbind(rows, data.frame(computation = computation, inputs = inputs, value = as.character(value), + stringsAsFactors = FALSE)) + } + + label <- function(key) { + if (is.null(key)) return("?") + meta <- .sliceMeta(key, datasets) + paste0(meta$name, " (slice ", key, ")") + } + + # 1. interaction terms (Zoo D): product of the components within their shared slice + inter <- options$interactionD + if (!is.null(inter) && is.list(inter)) { + interRows <- if (!is.null(inter$value)) inter$value else inter + for (row in interRows) { + comps <- as.character(unlist(row$components %||% list())) + if (length(comps) < 2) next + keys <- unique(vapply(comps, function(c) { k <- .resolveSlice(c, datasets) + if (is.null(k)) "-" else k }, character(1))) + joint <- if (length(keys) == 1 && keys[[1]] != "-") keys[[1]] else NULL + value <- "" + if (!is.null(joint)) { + cols <- lapply(comps, function(c) .columnByToken(c, datasets, joint)) + if (all(vapply(cols, function(x) !is.null(x) && is.numeric(x$values), logical(1))) && + length(unique(vapply(cols, function(x) length(x$values), integer(1)))) == 1) { + prod <- Reduce(`*`, lapply(cols, function(x) x$values)) + value <- sprintf("mean of product %.3f (n=%d)", mean(prod, na.rm = TRUE), sum(!is.na(prod))) + } else value <- "components not numeric / unequal lengths" + } else value <- paste("components span slices:", paste(keys, collapse = "/")) + rows <- addRow2("interaction product", + paste0(paste(comps, collapse = " : "), " in ", label(joint)), value) + } + } + + # 2. cross-host column product: assignedA's first scale column x assignedDefault's first scale + # column (the default-provider list - a bare -1 reference resolves through the analysis + # fallback, so the product joins a stamped slice with the fallback slice) + colA <- .firstAssignedScale(options$assignedA, datasets) + colB <- .firstAssignedScale(options$assignedDefault, datasets) + if (!is.null(colA) && !is.null(colB)) { + value <- if (length(colA$values) != length(colB$values)) + paste0("row lengths differ (", length(colA$values), " vs ", length(colB$values), + "): no join semantics - whole-column aggregates only") + else sprintf("mean of product %.3f", mean(colA$values * colB$values, na.rm = TRUE)) + rows <- addRow2("cross-host column product", + paste0(colA$column, " of ", label(colA$key), " x ", colB$column, " of ", label(colB$key)), + value) + } + + # 3. tableL multipliers (Zoo L) applied to every slice's first scale column + tableL <- .tableViewOption(options$tableL) + if (!is.null(tableL) && ncol(tableL$values) >= 1) { + mult <- mean(tableL$values[, 1], na.rm = TRUE) + scaleCol <- .firstScaleColumn(datasets) + if (!is.null(scaleCol) && is.finite(mult)) + rows <- addRow2("tableview multipliers", + paste0("mean(multiplier)=", sprintf("%.2f", mult), " x ", scaleCol$column, + " of ", label(scaleCol$key)), + sprintf("scaled mean %.3f", mult * mean(scaleCol$values, na.rm = TRUE))) + } + + # 4. scale factor (Zoo H real field) + scaleCol <- .firstScaleColumn(datasets) + doubleH <- .numOption(options$doubleH) + if (!is.null(scaleCol) && is.finite(doubleH)) + rows <- addRow2("real-field scaling", + paste0("doubleH=", doubleH, " x ", scaleCol$column, " of ", label(scaleCol$key)), + sprintf("scaled mean %.3f", doubleH * mean(scaleCol$values, na.rm = TRUE))) + + # 5. radio-driven statistic (Zoo H radio group) on the comboA column + statCol <- .columnByToken(.strOption(options$comboA, ""), datasets) + if (!is.null(statCol) && is.numeric(statCol$values)) + rows <- addRow2("radio-driven statistic", + paste0(.strOption(options$radioH, "mean"), " of ", statCol$column, " of ", label(statCol$key)), + .statLabel(.strOption(options$radioH, "mean"), statCol$values)) + + if (nrow(rows) == 0) + rows <- addRow2("(no computation)", "(nothing assigned yet)", + "assign columns/lists to see cross-slice computations here") + + table$setData(rows) +} + +# ------------------------------------------------------------------ encodeThis applied (c) +# Zoo E's factor/level names (extras roundtrip) APPLIED to a real column: the recode proves the +# names came back decoded AND usable. The factor name arrives extras-encoded (JaspExtraOptions_*, +# decoded on the way out); to USE it R-side it is decoded through the same strict table +# (jaspBase::decodeName) - the sanctioned helper, not a re-implementation of the token grammar. +encodeDemoTable <- function(jaspResults, options, datasets) { + table <- createJaspTable("encodeThis recode demo (Zoo E levels applied)") + jaspResults[["encodeDemo"]] <- table + + table$addColumnInfo("factor", title = "Factor (decoded)") + table$addColumnInfo("slice", title = "Column found in slice") + table$addColumnInfo("oldValue", title = "Original value") + table$addColumnInfo("newLabel", title = "Recoded to level") + table$addColumnInfo("n", title = "n") + + rows <- data.frame(factor = character(), slice = character(), oldValue = character(), + newLabel = character(), n = integer(), stringsAsFactors = FALSE) + + .decodeNameSafe <- function(x) tryCatch(jaspBase::decodeName(x), error = function(e) x) + + factors <- options$factorsE + if (!is.null(factors) && is.list(factors)) + for (f in factors) { + if (!is.list(f) || is.null(f$name)) next + fname <- .decodeNameSafe(as.character(f$name)) + levels <- as.character(unlist(f$levels %||% list())) + levels <- if (length(levels)) vapply(levels, .decodeNameSafe, character(1)) else levels + # The slice data.frames carry ENCODED column tokens (that is what R receives) - the + # decoded factor name must be matched against the DECODED column names, not the raw + # colnames (the verifier caught the first version matching "Group" against tokens). + hitKey <- NULL; hitTok <- NULL + for (k in names(datasets)) { + toks <- names(datasets[[k]]) + dec <- vapply(toks, function(t) .decodeNameSafe(t), character(1)) + i <- match(fname, dec) + if (!is.na(i)) { hitKey <- k; hitTok <- toks[[i]]; break } + } + if (is.null(hitKey)) { + rows <- rbind(rows, data.frame(factor = fname, slice = "(no column of this name in any slice)", + oldValue = "-", newLabel = "-", n = NA_integer_, + stringsAsFactors = FALSE)) + next + } + values <- as.character(stats::na.omit(datasets[[hitKey]][[hitTok]])) + uniq <- unique(values) + recode <- if (length(levels)) levels[pmin(match(values, uniq), length(levels))] else values + for (u in uniq) { + n <- sum(values == u) + rows <- rbind(rows, data.frame(factor = fname, slice = hitKey, oldValue = u, + newLabel = recode[match(u, values)][1], n = n, + stringsAsFactors = FALSE)) + } + } + + if (nrow(rows) == 0) + rows <- data.frame(factor = "(no factors)", slice = "-", oldValue = "-", newLabel = "-", + n = NA_integer_, stringsAsFactors = FALSE) + + table$setData(rows) +} + +# ------------------------------------------------------------------ checkbox-gated sub-result (c) +# Exists ONLY while gateH is open - a gate means absence, not an empty table. +gatedResultTable <- function(jaspResults, options, datasets) { + threshold <- .numOption(options$gatedH, 0) + # dependencies = "gateH": the gate family's destruction semantics. jaspBase destroys an object + # whose dependencies changed before the next run, so flipping gateH off removes the table and + # the gate being closed means this child is ABSENT - not an empty table. (Without deps the + # object from the previous run persists forever, which the verifier caught.) + table <- createJaspTable(paste0("Gated sub-result (threshold ", threshold, ")"), dependencies = "gateH") + jaspResults[["gatedResult"]] <- table + + table$addColumnInfo("slice", title = "Slice") + table$addColumnInfo("column", title = "First scale column") + table$addColumnInfo("nAbove", title = "n above threshold") + table$addColumnInfo("meanAbove", title = "mean above threshold") + + rows <- data.frame(slice = character(), column = character(), nAbove = integer(), + meanAbove = numeric(), stringsAsFactors = FALSE) + if (!is.null(datasets)) + for (key in names(datasets)) { + col <- .firstScaleColumn(datasets, key) + if (is.null(col)) next + above <- col$values[col$values > threshold & !is.na(col$values)] + rows <- rbind(rows, data.frame( + slice = paste0(key, " (", .sliceMeta(key, datasets)$name, ")"), column = col$column, + nAbove = length(above), + meanAbove = if (length(above)) mean(above) else NA_real_, + stringsAsFactors = FALSE)) + } + + table$setData(rows) +} + +# ------------------------------------------------------------------ more internals + +# a bound tableview option as R receives it: list of columns {name, levels, values} -> matrix +.tableViewOption <- function(tv) { + if (is.null(tv) || !is.list(tv) || !length(tv)) return(NULL) + vals <- lapply(tv, function(col) { + v <- if (is.list(col)) col$values else col + suppressWarnings(as.numeric(unlist(v))) + }) + lens <- vapply(vals, length, integer(1)) + len <- max(lens) + if (is.null(len) || is.na(len) || len == 0) return(NULL) + vals <- lapply(vals, function(v) { length(v) <- len; v }) + m <- matrix(unlist(vals), nrow = len, ncol = length(vals)) + list(values = m, + colNames = vapply(tv, function(c) if (is.list(c) && !is.null(c$name)) as.character(c$name) else "-", + character(1))) +} + +# a column by its (encoded) token, optionally within one slice, with its provenance +.columnByToken <- function(token, datasets, key = NULL) { + if (!nzchar(token) || is.null(datasets)) return(NULL) + key <- if (is.null(key)) .resolveSlice(token, datasets) else key + if (is.null(key)) return(NULL) + col <- datasets[[key]][[token]] + if (is.null(col)) return(NULL) + list(key = key, column = token, values = if (is.factor(col)) as.numeric(col) else col) +} + +# the first numeric column of an assigned list, with its slice provenance +.firstAssignedScale <- function(option, datasets) { + for (token in .dependentValues(option)) { + col <- .columnByToken(token, datasets) + if (!is.null(col) && is.numeric(col$values) && sum(!is.na(col$values)) > 1) + return(col) + } + NULL +} diff --git a/R/multiDataSetControlsFuncWrapper.R b/R/multiDataSetControlsFuncWrapper.R new file mode 100644 index 0000000..cbb3818 --- /dev/null +++ b/R/multiDataSetControlsFuncWrapper.R @@ -0,0 +1,105 @@ +# +# Copyright (C) 2013-2025 University of Amsterdam +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +# This is a generated file. Don't change it! + +#' Test Multi-DataSet Controls +#' +#' @param datasets a named list of data.frames keyed by DATASET TITLE - the titles the +#' dataset selection options of this form carry (their defaults below, rendered as the +#' selection surface, e.g. "Iris" or "Iris - myfilter"). Every entry is loaded standalone +#' into the bridge workspace (runWrappedAnalysis stores them via jaspSyntax::loadDataSets, +#' each under its own name as the dataset title); the analysis itself receives its slices +#' keyed by filter id - the jaspBase datasets contract (see jaspBase::runJaspResults). +multiDataSetControlsFunc <- function( + datasets = NULL, + version = "1", + assignedA = list(types = list(), value = list()), + assignedDefault = list(types = list(), value = list()), + assignedDerived = list(types = list(), value = list()), + assignedK = list(types = list(), value = list()), + assignedThree = list(types = list(), value = list()), + boundG = list(types = list(), value = list()), + codeF = list(columns = list(), model = "", modelOriginal = ""), + comboA = list(types = "unknown", value = ""), + comboG = "first", + comboI = list(types = "unknown", value = ""), + dataSetA = "", + dataSetD = "", + dataSetF = "", + dataSetG = "", + dataSetJ = "", + dataSetK = "", + dataSetM3 = "", + doubleH = 1.5, + factorsE = list(), + factorsK = list(list(indicators = list(), name = "Fac1", title = "Factor 1")), + gateH = FALSE, + gatedH = 10, + interactionD = list(optionKey = "components", types = list(), value = list()), + pairsJ = list(), + plainSwitch = FALSE, + plotHeight = 320, + plotWidth = 480, + radioH = "mean", + rowsC = list(optionKey = "variable", types = list(), value = list()), + rowsH = 5, + showMeta = FALSE, + sliderH = 0.5, + tableL = list(list(levels = list("Row 0", "Row 1", "Row 2", "Row 3", "Row 4"), name = "data 1", values = list(1, 2, 3, 4, 5)), list(levels = list("Row 0", "Row 1", "Row 2", "Row 3", "Row 4"), name = "data 2", values = list(0.5, 1, 1.5, 2, 2.5))), + textH = "") { + + defaultArgCalls <- formals(jaspTestModule::multiDataSetControlsFunc) + defaultArgs <- lapply(defaultArgCalls, eval) + options <- as.list(match.call())[-1L] + options <- lapply(options, eval) + defaults <- setdiff(names(defaultArgs), names(options)) + options[defaults] <- defaultArgs[defaults] + options[["datasets"]] <- NULL + options[["version"]] <- NULL + + + # `datasets` must be a named list of data.frames keyed by DATASET TITLE - the + # titles the dataset selection options of this form carry (e.g. list(Iris = iris, Sleep = sleep)). + # No positional pin: the dataset that plays `data` (the frame the formula machinery evaluates + # against) is the first selection option whose effective value names one of the loaded datasets, + # resolved at run time from `options`. Every entry is loaded standalone by runWrappedAnalysis + # (jaspBase::storeDataSets -> jaspSyntax::loadDataSets, each under its own name as the dataset + # title); the analysis itself receives its slices keyed by filter id (jaspBase datasets contract). + selectionOptions <- c("dataSetK", "dataSetG", "dataSetF", "dataSetJ", "dataSetM3", "dataSetA", "dataSetD") + data <- NULL + if (!is.null(datasets) && length(datasets)) { + for (selectionOption in selectionOptions) { + selectionValue <- options[[selectionOption]] + if (!is.character(selectionValue) || length(selectionValue) != 1L || is.na(selectionValue) || !nzchar(selectionValue)) next + if (selectionValue %in% names(datasets)) { data <- datasets[[selectionValue]]; break } + bareTitle <- sub(" - [^-]*$", "", selectionValue) # 'Title - filterName' surface: bare-title fallback, best effort + if (bareTitle %in% names(datasets)) { data <- datasets[[bareTitle]]; break } + } + if (is.null(data)) { + if (length(selectionOptions)) + stop("`datasets` must be a named list keyed by dataset title (e.g. list(Iris = iris)): the dataset selection option(s) ", paste(selectionOptions, collapse = ", "), " select a title matching none of: ", paste(names(datasets), collapse = ", ")) + data <- datasets[[1L]] # this form has no dataset selection surface: the first entry is the documented primary (jaspSyntax::loadDataSets) + } + } + + optionsWithFormula <- c("assignedA", "assignedDefault", "assignedDerived", "assignedK", "assignedThree", "boundG", "codeF", "comboA", "comboG", "comboI", "dataSetA", "dataSetD", "dataSetF", "dataSetG", "dataSetJ", "dataSetK", "dataSetM3", "factorsE", "factorsK", "interactionD", "pairsJ", "rowsC", "tableL") + for (name in optionsWithFormula) { + if ((name %in% optionsWithFormula) && inherits(options[[name]], "formula")) options[[name]] = jaspBase::jaspFormula(options[[name]], data) } + + return(jaspBase::runWrappedAnalysis("jaspTestModule", "multiDataSetControlsFunc", "testMultiDataSetControls.qml", options, version, TRUE, datasets = datasets)) +} \ No newline at end of file diff --git a/R/multiDataSetFuncWrapper.R b/R/multiDataSetFuncWrapper.R new file mode 100644 index 0000000..dfbcfa6 --- /dev/null +++ b/R/multiDataSetFuncWrapper.R @@ -0,0 +1,79 @@ +# +# Copyright (C) 2013-2025 University of Amsterdam +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +# This is a generated file. Don't change it! + +#' Test Multi-DataSet +#' +#' @param datasets a named list of data.frames keyed by DATASET TITLE - the titles the +#' dataset selection options of this form carry (their defaults below, rendered as the +#' selection surface, e.g. "Iris" or "Iris - myfilter"). Every entry is loaded standalone +#' into the bridge workspace (runWrappedAnalysis stores them via jaspSyntax::loadDataSets, +#' each under its own name as the dataset title); the analysis itself receives its slices +#' keyed by filter id - the jaspBase datasets contract (see jaspBase::runJaspResults). +multiDataSetFunc <- function( + datasets = NULL, + version = "1", + dataSetA = "", + dataSetB = "", + dependentA = list(types = list(), value = list()), + dependentB = list(types = list(), value = list()), + guardDemo = FALSE, + plotHeight = 320, + plotWidth = 480, + showMeta = FALSE) { + + defaultArgCalls <- formals(jaspTestModule::multiDataSetFunc) + defaultArgs <- lapply(defaultArgCalls, eval) + options <- as.list(match.call())[-1L] + options <- lapply(options, eval) + defaults <- setdiff(names(defaultArgs), names(options)) + options[defaults] <- defaultArgs[defaults] + options[["datasets"]] <- NULL + options[["version"]] <- NULL + + + # `datasets` must be a named list of data.frames keyed by DATASET TITLE - the + # titles the dataset selection options of this form carry (e.g. list(Iris = iris, Sleep = sleep)). + # No positional pin: the dataset that plays `data` (the frame the formula machinery evaluates + # against) is the first selection option whose effective value names one of the loaded datasets, + # resolved at run time from `options`. Every entry is loaded standalone by runWrappedAnalysis + # (jaspBase::storeDataSets -> jaspSyntax::loadDataSets, each under its own name as the dataset + # title); the analysis itself receives its slices keyed by filter id (jaspBase datasets contract). + selectionOptions <- c("dataSetA", "dataSetB") + data <- NULL + if (!is.null(datasets) && length(datasets)) { + for (selectionOption in selectionOptions) { + selectionValue <- options[[selectionOption]] + if (!is.character(selectionValue) || length(selectionValue) != 1L || is.na(selectionValue) || !nzchar(selectionValue)) next + if (selectionValue %in% names(datasets)) { data <- datasets[[selectionValue]]; break } + bareTitle <- sub(" - [^-]*$", "", selectionValue) # 'Title - filterName' surface: bare-title fallback, best effort + if (bareTitle %in% names(datasets)) { data <- datasets[[bareTitle]]; break } + } + if (is.null(data)) { + if (length(selectionOptions)) + stop("`datasets` must be a named list keyed by dataset title (e.g. list(Iris = iris)): the dataset selection option(s) ", paste(selectionOptions, collapse = ", "), " select a title matching none of: ", paste(names(datasets), collapse = ", ")) + data <- datasets[[1L]] # this form has no dataset selection surface: the first entry is the documented primary (jaspSyntax::loadDataSets) + } + } + + optionsWithFormula <- c("dataSetA", "dataSetB", "dependentA", "dependentB") + for (name in optionsWithFormula) { + if ((name %in% optionsWithFormula) && inherits(options[[name]], "formula")) options[[name]] = jaspBase::jaspFormula(options[[name]], data) } + + return(jaspBase::runWrappedAnalysis("jaspTestModule", "multiDataSetFunc", "testMultiDataSet.qml", options, version, TRUE, datasets = datasets)) +} \ No newline at end of file diff --git a/R/radioButtonFuncWrapper.R b/R/radioButtonFuncWrapper.R new file mode 100644 index 0000000..f379a6c --- /dev/null +++ b/R/radioButtonFuncWrapper.R @@ -0,0 +1,57 @@ +# +# Copyright (C) 2013-2025 University of Amsterdam +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +# This is a generated file. Don't change it! + +#' Radio Button +#' +radioButtonFunc <- function( + data = NULL, + version = "1", + assigned = list(types = list(), value = list()), + buttons1 = list(list(value = "button1"), list(value = "button2")), + buttons2 = list(list(value = "button1"), list(value = "button2")), + dynamic = list(), + dynamicGroupWithDynamicButtons = list(list(value = "grouo1"), list(value = "group2")), + dynamicGroupWithStaticButtons = list(list(value = "grouo1"), list(value = "group2")), + firstGroup = "one", + plotHeight = 320, + plotWidth = 480, + secondGroup = "one", + staticGroupWithDynamicButtons = "", + staticGroupWithStaticAndDynamicButtons = "Static button") { + + defaultArgCalls <- formals(jaspTestModule::radioButtonFunc) + defaultArgs <- lapply(defaultArgCalls, eval) + options <- as.list(match.call())[-1L] + options <- lapply(options, eval) + defaults <- setdiff(names(defaultArgs), names(options)) + options[defaults] <- defaultArgs[defaults] + options[["data"]] <- NULL + options[["version"]] <- NULL + + + if (!jaspBase::jaspResultsCalledFromJasp() && !is.null(data)) { + jaspBase::storeDataSet(data) + } + + optionsWithFormula <- c("assigned", "buttons1", "buttons2", "dynamic", "dynamicGroupWithDynamicButtons", "dynamicGroupWithStaticButtons") + for (name in optionsWithFormula) { + if ((name %in% optionsWithFormula) && inherits(options[[name]], "formula")) options[[name]] = jaspBase::jaspFormula(options[[name]], data) } + + return(jaspBase::runWrappedAnalysis("jaspTestModule", "radioButtonFunc", "RadioButtonTest.qml", options, version, FALSE)) +} \ No newline at end of file diff --git a/R/syntaxhighlighting.R b/R/syntaxhighlighting.R index 54e6ac4..7f0fbc0 100644 --- a/R/syntaxhighlighting.R +++ b/R/syntaxhighlighting.R @@ -1,5 +1,5 @@ -syntaxhighlightingFunc <- function(jaspResults, dataset, options) +syntaxhighlightingFuncInternal <- function(jaspResults, dataset, options) { tableOfOptions(jaspResults, options, dataset) -} \ No newline at end of file +} diff --git a/R/syntaxhighlightingFuncWrapper.R b/R/syntaxhighlightingFuncWrapper.R new file mode 100644 index 0000000..28d2530 --- /dev/null +++ b/R/syntaxhighlightingFuncWrapper.R @@ -0,0 +1,49 @@ +# +# Copyright (C) 2013-2025 University of Amsterdam +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +# This is a generated file. Don't change it! + +#' Syntax Highlighting +#' +syntaxhighlightingFunc <- function( + data = NULL, + version = "1", + R = list(columns = list(), model = "", modelOriginal = ""), + plotHeight = 320, + plotWidth = 480, + syntax = list(columns = list(), model = "", modelOriginal = "")) { + + defaultArgCalls <- formals(jaspTestModule::syntaxhighlightingFunc) + defaultArgs <- lapply(defaultArgCalls, eval) + options <- as.list(match.call())[-1L] + options <- lapply(options, eval) + defaults <- setdiff(names(defaultArgs), names(options)) + options[defaults] <- defaultArgs[defaults] + options[["data"]] <- NULL + options[["version"]] <- NULL + + + if (!jaspBase::jaspResultsCalledFromJasp() && !is.null(data)) { + jaspBase::storeDataSet(data) + } + + optionsWithFormula <- c("R", "syntax") + for (name in optionsWithFormula) { + if ((name %in% optionsWithFormula) && inherits(options[[name]], "formula")) options[[name]] = jaspBase::jaspFormula(options[[name]], data) } + + return(jaspBase::runWrappedAnalysis("jaspTestModule", "syntaxhighlightingFunc", "SyntaxHighlighting.qml", options, version, FALSE)) +} \ No newline at end of file diff --git a/R/testAnalysisColumnFuncWrapper.R b/R/testAnalysisColumnFuncWrapper.R new file mode 100644 index 0000000..5bbe5ed --- /dev/null +++ b/R/testAnalysisColumnFuncWrapper.R @@ -0,0 +1,49 @@ +# +# Copyright (C) 2013-2025 University of Amsterdam +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +# This is a generated file. Don't change it! + +#' Test (comp) columns from analyses +#' +testAnalysisColumnFunc <- function( + data = NULL, + version = "1", + AddColumnField = "", + AddColumnsPattern = "", + AddThisManyColumns = 3, + ComputedColumnField = "", + createColumnInR = TRUE, + plotHeight = 320, + plotWidth = 480) { + + defaultArgCalls <- formals(jaspTestModule::testAnalysisColumnFunc) + defaultArgs <- lapply(defaultArgCalls, eval) + options <- as.list(match.call())[-1L] + options <- lapply(options, eval) + defaults <- setdiff(names(defaultArgs), names(options)) + options[defaults] <- defaultArgs[defaults] + options[["data"]] <- NULL + options[["version"]] <- NULL + + + if (!jaspBase::jaspResultsCalledFromJasp() && !is.null(data)) { + jaspBase::storeDataSet(data) + } + + + return(jaspBase::runWrappedAnalysis("jaspTestModule", "testAnalysisColumnFunc", "testAnalysisColumnFunc.qml", options, version, FALSE)) +} \ No newline at end of file diff --git a/R/testAnalysisColumns.R b/R/testAnalysisColumns.R index bfba7cb..4bb0428 100644 --- a/R/testAnalysisColumns.R +++ b/R/testAnalysisColumns.R @@ -1,4 +1,4 @@ -testAnalysisColumnFunc <- function(jaspResults, dataset, options) +testAnalysisColumnFuncInternal <- function(jaspResults, dataset, options) { tableOfOptions(jaspResults, options, dataset) @@ -76,4 +76,4 @@ createAndFillAJaspColumn <- function(container, name, dependencies) col$setScale(generateDataForColumn()) container[[name]] <- col -} \ No newline at end of file +} diff --git a/R/testComponentsListLayout.R b/R/testComponentsListLayout.R index fbf7198..a6c2072 100644 --- a/R/testComponentsListLayout.R +++ b/R/testComponentsListLayout.R @@ -1,5 +1,5 @@ -testComponentsListLayout <- function(jaspResults, dataset, options) +testComponentsListLayoutInternal <- function(jaspResults, dataset, options) { tableOfOptions(jaspResults, options, dataset) } diff --git a/R/testComponentsListLayoutWrapper.R b/R/testComponentsListLayoutWrapper.R new file mode 100644 index 0000000..a8588ec --- /dev/null +++ b/R/testComponentsListLayoutWrapper.R @@ -0,0 +1,61 @@ +# +# Copyright (C) 2013-2025 University of Amsterdam +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +# This is a generated file. Don't change it! + +#' Test ComponentsList & TableView layout +#' +testComponentsListLayout <- function( + data = NULL, + version = "1", + addBorderCheck = TRUE, + addItemManuallyCheck = TRUE, + buttonsInRowCheck = FALSE, + columns = "3", + componentsList = list(local({ .l <- list("#", "Second column", "Third column", "First hypothesis", "#"); names(.l) <- c("", "col2", "col3", "hypothesis", "value"); .l }), local({ .l <- list("#2", "Second hypothesis", "#2"); names(.l) <- c("", "hypothesis", "value"); .l }), list(col2 = "col 2", col3 = "col 3", col4 = "col 4", col5 = "col 5", col6 = "col 6", hypothesis = "Hypothesis 3", value = "#3"), list(col2 = "col 2", col3 = "col 3", col4 = "col 4", col5 = "col 5", col6 = "col 6", hypothesis = "Hypothesis 4", value = "#4")), + contrasts = list(optionKey = "variable", types = list(), value = list()), + customContrasts = list(), + fixedFactors = list(optionKey = "components", types = list(), value = list()), + headers = "Header with array", + heightCheck = FALSE, + plotHeight = 320, + plotWidth = 480, + showButtonsCheck = TRUE, + showTitleCheck = FALSE, + tableHeightCheck = FALSE, + tableView = list(list(levels = list(), name = "data 1", values = list()), list(levels = list(), name = "data 2", values = list()))) { + + defaultArgCalls <- formals(jaspTestModule::testComponentsListLayout) + defaultArgs <- lapply(defaultArgCalls, eval) + options <- as.list(match.call())[-1L] + options <- lapply(options, eval) + defaults <- setdiff(names(defaultArgs), names(options)) + options[defaults] <- defaultArgs[defaults] + options[["data"]] <- NULL + options[["version"]] <- NULL + + + if (!jaspBase::jaspResultsCalledFromJasp() && !is.null(data)) { + jaspBase::storeDataSet(data) + } + + optionsWithFormula <- c("columns", "componentsList", "contrasts", "customContrasts", "fixedFactors", "headers", "tableView") + for (name in optionsWithFormula) { + if ((name %in% optionsWithFormula) && inherits(options[[name]], "formula")) options[[name]] = jaspBase::jaspFormula(options[[name]], data) } + + return(jaspBase::runWrappedAnalysis("jaspTestModule", "testComponentsListLayout", "testComponentsListLayout.qml", options, version, FALSE)) +} \ No newline at end of file diff --git a/R/testContainer.R b/R/testContainer.R deleted file mode 100644 index ca8a196..0000000 --- a/R/testContainer.R +++ /dev/null @@ -1,20 +0,0 @@ -testContainerFunc <- function(jaspResults, dataset, options) -{ - tableOfOptions(jaspResults, options, dataset) - - cnt <- addContainer(addContainer(addContainer(addContainer(jaspResults, name="c0")))) - testFootnotesTableFunc(cnt, dataset, options) - - cnt <- addContainer(addContainer(addContainer(addContainer(jaspResults, name="c1")), title="hallo")) - testFootnotesTableFunc(cnt, dataset, options) - - cnt <- addContainer(addContainer(addContainer(addContainer(jaspResults, title="hallo", name="c2")), title="er zit er eentje stiekem om me heen"), title="hallo", initCollapsed=TRUE) - testFootnotesTableFunc(cnt, dataset, options) -} - -addContainer <- function(inContainer, name="container", title="", initCollapsed=FALSE) -{ - inContainer[[name]] = createJaspContainer(title=title, initCollapsed=initCollapsed) - - return(inContainer[[name]]) -} diff --git a/R/testControlsLayout.R b/R/testControlsLayout.R index 41aca61..df8cc47 100644 --- a/R/testControlsLayout.R +++ b/R/testControlsLayout.R @@ -1,5 +1,5 @@ -testControlsLayoutFunc <- function(jaspResults, dataset, options) +testControlsLayoutFuncInternal <- function(jaspResults, dataset, options) { tableOfOptions(jaspResults, options, dataset) diff --git a/R/testControlsLayoutFuncWrapper.R b/R/testControlsLayoutFuncWrapper.R new file mode 100644 index 0000000..17f27d8 --- /dev/null +++ b/R/testControlsLayoutFuncWrapper.R @@ -0,0 +1,96 @@ +# +# Copyright (C) 2013-2025 University of Amsterdam +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +# This is a generated file. Don't change it! + +#' Test Controls layout +#' +#' @param meanCiLevel, width of the confidence interval. +#' @param meanCiMethod, How should the confidence interval be computed? By default, we use a `T model`, which yields results identical to a one-sample t-test. Alternative options are a normal model ($\bar{x} \pm z_{95} \times SE$), or `Bootstrap`. +#' @param sdCi, a confidence interval for the standard deviation based on bootstrap samples. +#' Defaults to \code{FALSE}. +testControlsLayoutFunc <- function( + data = NULL, + version = "1", + checkBoxSecondLevel = FALSE, + checkboxWithSeveralLevels = FALSE, + doubleInGroup = 0, + dropDownInGroup = list(types = "unknown", value = ""), + dropDownNominal = list(types = "unknown", value = ""), + dropDownOrdinal = list(types = "unknown", value = ""), + dropDownScale = list(types = "unknown", value = ""), + dropDownSecondLevel = "one", + dropDownStartValue = "really long values!!!!!", + dropDownValues = "one", + dropDownValuesAbove = "one", + dropDownValuesEmptyValue = "", + dropDownValuesFixed = "one", + dropDownValuesFixedAbove = "one", + dropDownValuesFixedEmptyValue = "", + dropDownValuesFixedStartValue = "really long values!!!!!", + dropDownVariables = list(types = "unknown", value = ""), + dropDownVariablesEmptyValue = "", + dropDownVariablesFixed = list(types = "unknown", value = ""), + dropDownVariablesFixedEmptyValue = list(types = "unknown", value = ""), + dropDownVariablesFixedLabelAbove = list(types = "unknown", value = ""), + dropDownVariablesFixedStartIndex = list(types = "unknown", value = ""), + dropDownVariablesLabel = list(types = "unknown", value = ""), + dropDownVariablesStartIndex = list(types = "unknown", value = ""), + intValue = 0, + integerFirstLevel = 0, + integerSecondLevel = 0, + maxLevels = 100, + maxNumeric = 100, + meanCi = FALSE, + meanCiLevel = 0.95, + meanCiMethod = "oneSampleTTest", + minLevels = -1, + minNumeric = -1, + percentFirstLevel = 0.5, + percentileValues = list(), + percentiles = FALSE, + plotHeight = 320, + plotWidth = 480, + quantilesForEqualGroups = FALSE, + quantilesForEqualGroupsNumber = 0, + quartiles = FALSE, + sdCi = FALSE, + sdCiLevel = 0.95, + sdCiMethod = "chiSquaredModel", + seMean = FALSE, + textInGroup = "") { + + defaultArgCalls <- formals(jaspTestModule::testControlsLayoutFunc) + defaultArgs <- lapply(defaultArgCalls, eval) + options <- as.list(match.call())[-1L] + options <- lapply(options, eval) + defaults <- setdiff(names(defaultArgs), names(options)) + options[defaults] <- defaultArgs[defaults] + options[["data"]] <- NULL + options[["version"]] <- NULL + + + if (!jaspBase::jaspResultsCalledFromJasp() && !is.null(data)) { + jaspBase::storeDataSet(data) + } + + optionsWithFormula <- c("dropDownInGroup", "dropDownNominal", "dropDownOrdinal", "dropDownScale", "dropDownSecondLevel", "dropDownStartValue", "dropDownValues", "dropDownValuesAbove", "dropDownValuesEmptyValue", "dropDownValuesFixed", "dropDownValuesFixedAbove", "dropDownValuesFixedEmptyValue", "dropDownValuesFixedStartValue", "dropDownVariables", "dropDownVariablesEmptyValue", "dropDownVariablesFixed", "dropDownVariablesFixedEmptyValue", "dropDownVariablesFixedLabelAbove", "dropDownVariablesFixedStartIndex", "dropDownVariablesLabel", "dropDownVariablesStartIndex", "meanCiMethod", "sdCiMethod") + for (name in optionsWithFormula) { + if ((name %in% optionsWithFormula) && inherits(options[[name]], "formula")) options[[name]] = jaspBase::jaspFormula(options[[name]], data) } + + return(jaspBase::runWrappedAnalysis("jaspTestModule", "testControlsLayoutFunc", "testControlsLayout.qml", options, version, FALSE)) +} \ No newline at end of file diff --git a/R/testCrapDataTable.R b/R/testCrapDataTable.R index 79811b0..94aac5f 100644 --- a/R/testCrapDataTable.R +++ b/R/testCrapDataTable.R @@ -1,6 +1,6 @@ -testCrapDataTable <- function(jaspResults, dataset, options) +testCrapDataTableInternal <- function(jaspResults, dataset, options) { tableOfOptions(jaspResults, options, dataset) @@ -36,4 +36,4 @@ testCrapDataTable <- function(jaspResults, dataset, options) jaspResults[["hereYaGo"]] <- tb -} \ No newline at end of file +} diff --git a/R/testCrapDataTableWrapper.R b/R/testCrapDataTableWrapper.R new file mode 100644 index 0000000..b096bc9 --- /dev/null +++ b/R/testCrapDataTableWrapper.R @@ -0,0 +1,45 @@ +# +# Copyright (C) 2013-2025 University of Amsterdam +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +# This is a generated file. Don't change it! + +#' Test jaspTable with crappy data +#' +testCrapDataTable <- function( + data = NULL, + version = "1", + checkbox_0 = FALSE, + plotHeight = 320, + plotWidth = 480) { + + defaultArgCalls <- formals(jaspTestModule::testCrapDataTable) + defaultArgs <- lapply(defaultArgCalls, eval) + options <- as.list(match.call())[-1L] + options <- lapply(options, eval) + defaults <- setdiff(names(defaultArgs), names(options)) + options[defaults] <- defaultArgs[defaults] + options[["data"]] <- NULL + options[["version"]] <- NULL + + + if (!jaspBase::jaspResultsCalledFromJasp() && !is.null(data)) { + jaspBase::storeDataSet(data) + } + + + return(jaspBase::runWrappedAnalysis("jaspTestModule", "testCrapDataTable", "testCrapDataTable.qml", options, version, FALSE)) +} \ No newline at end of file diff --git a/R/testCustomContrasts.R b/R/testCustomContrasts.R index 1354682..b18bfc1 100644 --- a/R/testCustomContrasts.R +++ b/R/testCustomContrasts.R @@ -1,4 +1,4 @@ -customContrasts <- function(jaspResults, dataset, options) +customContrastsInternal <- function(jaspResults, dataset, options) { tableOfOptions(jaspResults, options, dataset) @@ -37,4 +37,4 @@ createCustomConstrastsTable <- function(customContrasts) return(table); -} \ No newline at end of file +} diff --git a/R/testDefaultValuesWithBindings.R b/R/testDefaultValuesWithBindings.R index 2122414..052027d 100644 --- a/R/testDefaultValuesWithBindings.R +++ b/R/testDefaultValuesWithBindings.R @@ -1,4 +1,4 @@ -testDefaultValuesWithBindings <- function(jaspResults, dataset, options) +testDefaultValuesWithBindingsInternal <- function(jaspResults, dataset, options) { tableOfOptions(jaspResults, options, dataset) diff --git a/R/testDefaultValuesWithBindingsWrapper.R b/R/testDefaultValuesWithBindingsWrapper.R new file mode 100644 index 0000000..6955afc --- /dev/null +++ b/R/testDefaultValuesWithBindingsWrapper.R @@ -0,0 +1,51 @@ +# +# Copyright (C) 2013-2025 University of Amsterdam +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +# This is a generated file. Don't change it! + +#' Default values with bindings +#' +testDefaultValuesWithBindings <- function( + data = NULL, + version = "1", + assignedVariables = list(types = list(), value = list()), + check = FALSE, + drop = "none", + plotHeight = 320, + plotWidth = 480, + radio = "one") { + + defaultArgCalls <- formals(jaspTestModule::testDefaultValuesWithBindings) + defaultArgs <- lapply(defaultArgCalls, eval) + options <- as.list(match.call())[-1L] + options <- lapply(options, eval) + defaults <- setdiff(names(defaultArgs), names(options)) + options[defaults] <- defaultArgs[defaults] + options[["data"]] <- NULL + options[["version"]] <- NULL + + + if (!jaspBase::jaspResultsCalledFromJasp() && !is.null(data)) { + jaspBase::storeDataSet(data) + } + + optionsWithFormula <- c("assignedVariables", "drop") + for (name in optionsWithFormula) { + if ((name %in% optionsWithFormula) && inherits(options[[name]], "formula")) options[[name]] = jaspBase::jaspFormula(options[[name]], data) } + + return(jaspBase::runWrappedAnalysis("jaspTestModule", "testDefaultValuesWithBindings", "testDefaultValuesWithBindings.qml", options, version, FALSE)) +} \ No newline at end of file diff --git a/R/testEncoding.R b/R/testEncoding.R index 2185660..5c15af2 100644 --- a/R/testEncoding.R +++ b/R/testEncoding.R @@ -1,4 +1,4 @@ -testEncodingFunc <- function(jaspResults, dataset, options) +testEncodingFuncInternal <- function(jaspResults, dataset, options) { tableOfOptions(jaspResults, options, dataset) @@ -8,7 +8,7 @@ testEncodingFunc <- function(jaspResults, dataset, options) showData(dataset, jaspResults, "variablesPairs", unlist(options$pairs), "Pairs Variables") } -testEncodingPreloadDataFunc <- testEncodingFunc +testEncodingPreloadDataFuncInternal <- testEncodingFuncInternal showData <- function(dataset, jaspResults, name, cols, title, filter = "") { diff --git a/R/testEncodingFuncWrapper.R b/R/testEncodingFuncWrapper.R new file mode 100644 index 0000000..48091e5 --- /dev/null +++ b/R/testEncodingFuncWrapper.R @@ -0,0 +1,56 @@ +# +# Copyright (C) 2013-2025 University of Amsterdam +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +# This is a generated file. Don't change it! + +#' Test Variables encoding +#' +testEncodingFunc <- function( + data = NULL, + version = "1", + componentsListFromVariablesList = list(), + componentsListWithVariables = list(list(levelsOfSingleVariable = "", singleVariable = list(types = list(), value = ""), value = "#", variablesInComponentsList = list(types = list(), value = list()))), + dropdown = list(types = "unknown", value = ""), + genericFilter = "", + pairs = list(), + plotHeight = 320, + plotWidth = 480, + selected = list(types = list(), value = list()), + singleVariable = list(types = list(), value = ""), + variablesForComponentsList = list(optionKey = "components", types = list(), value = list()), + variablesWithInteractions = list(optionKey = "components", types = list(), value = list())) { + + defaultArgCalls <- formals(jaspTestModule::testEncodingFunc) + defaultArgs <- lapply(defaultArgCalls, eval) + options <- as.list(match.call())[-1L] + options <- lapply(options, eval) + defaults <- setdiff(names(defaultArgs), names(options)) + options[defaults] <- defaultArgs[defaults] + options[["data"]] <- NULL + options[["version"]] <- NULL + + + if (!jaspBase::jaspResultsCalledFromJasp() && !is.null(data)) { + jaspBase::storeDataSet(data) + } + + optionsWithFormula <- c("componentsListFromVariablesList", "componentsListWithVariables", "dropdown", "pairs", "selected", "singleVariable", "variablesForComponentsList", "variablesWithInteractions") + for (name in optionsWithFormula) { + if ((name %in% optionsWithFormula) && inherits(options[[name]], "formula")) options[[name]] = jaspBase::jaspFormula(options[[name]], data) } + + return(jaspBase::runWrappedAnalysis("jaspTestModule", "testEncodingFunc", "testEncoding.qml", options, version, FALSE)) +} \ No newline at end of file diff --git a/R/testEncodingPreloadDataFuncWrapper.R b/R/testEncodingPreloadDataFuncWrapper.R new file mode 100644 index 0000000..1c28c0c --- /dev/null +++ b/R/testEncodingPreloadDataFuncWrapper.R @@ -0,0 +1,56 @@ +# +# Copyright (C) 2013-2025 University of Amsterdam +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +# This is a generated file. Don't change it! + +#' Test Variables encoding with preloadData +#' +testEncodingPreloadDataFunc <- function( + data = NULL, + version = "1", + componentsListFromVariablesList = list(), + componentsListWithVariables = list(list(levelsOfSingleVariable = "", singleVariable = list(types = list(), value = ""), value = "#", variablesInComponentsList = list(types = list(), value = list()))), + dropdown = list(types = "unknown", value = ""), + genericFilter = "", + pairs = list(), + plotHeight = 320, + plotWidth = 480, + selected = list(types = list(), value = list()), + singleVariable = list(types = list(), value = ""), + variablesForComponentsList = list(optionKey = "components", types = list(), value = list()), + variablesWithInteractions = list(optionKey = "components", types = list(), value = list())) { + + defaultArgCalls <- formals(jaspTestModule::testEncodingPreloadDataFunc) + defaultArgs <- lapply(defaultArgCalls, eval) + options <- as.list(match.call())[-1L] + options <- lapply(options, eval) + defaults <- setdiff(names(defaultArgs), names(options)) + options[defaults] <- defaultArgs[defaults] + options[["data"]] <- NULL + options[["version"]] <- NULL + + + if (!jaspBase::jaspResultsCalledFromJasp() && !is.null(data)) { + jaspBase::storeDataSet(data) + } + + optionsWithFormula <- c("componentsListFromVariablesList", "componentsListWithVariables", "dropdown", "pairs", "selected", "singleVariable", "variablesForComponentsList", "variablesWithInteractions") + for (name in optionsWithFormula) { + if ((name %in% optionsWithFormula) && inherits(options[[name]], "formula")) options[[name]] = jaspBase::jaspFormula(options[[name]], data) } + + return(jaspBase::runWrappedAnalysis("jaspTestModule", "testEncodingPreloadDataFunc", "testEncoding.qml", options, version, TRUE)) +} \ No newline at end of file diff --git a/R/testFactorsForm.R b/R/testFactorsForm.R index 18e9b6c..d185201 100644 --- a/R/testFactorsForm.R +++ b/R/testFactorsForm.R @@ -1,5 +1,5 @@ -testFactorsFormFunc <- function(jaspResults, dataset, options) +testFactorsFormFuncInternal <- function(jaspResults, dataset, options) { tableOfOptions(jaspResults, options, dataset) diff --git a/R/testFactorsFormFuncWrapper.R b/R/testFactorsFormFuncWrapper.R new file mode 100644 index 0000000..d211646 --- /dev/null +++ b/R/testFactorsFormFuncWrapper.R @@ -0,0 +1,52 @@ +# +# Copyright (C) 2013-2025 University of Amsterdam +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +# This is a generated file. Don't change it! + +#' Test Factors Form +#' +testFactorsFormFunc <- function( + data = NULL, + version = "1", + factors = list(list(indicators = list(), name = "Factor1", title = "Factor 1")), + factorsList = list(list(keyLabel = "Factor 1", value = "Factor1")), + nested = TRUE, + nestedFactors = list(list(components = list(), name = "model0", title = "Model 0"), list(components = list(), name = "model1", title = "Model 1")), + plotHeight = 320, + plotWidth = 480, + variables = list(types = list(), value = list())) { + + defaultArgCalls <- formals(jaspTestModule::testFactorsFormFunc) + defaultArgs <- lapply(defaultArgCalls, eval) + options <- as.list(match.call())[-1L] + options <- lapply(options, eval) + defaults <- setdiff(names(defaultArgs), names(options)) + options[defaults] <- defaultArgs[defaults] + options[["data"]] <- NULL + options[["version"]] <- NULL + + + if (!jaspBase::jaspResultsCalledFromJasp() && !is.null(data)) { + jaspBase::storeDataSet(data) + } + + optionsWithFormula <- c("factors", "factorsList", "nestedFactors", "variables") + for (name in optionsWithFormula) { + if ((name %in% optionsWithFormula) && inherits(options[[name]], "formula")) options[[name]] = jaspBase::jaspFormula(options[[name]], data) } + + return(jaspBase::runWrappedAnalysis("jaspTestModule", "testFactorsFormFunc", "testFactorsForm.qml", options, version, FALSE)) +} \ No newline at end of file diff --git a/R/testFootnotesTable.R b/R/testFootnotesTable.R deleted file mode 100644 index edc9c9f..0000000 --- a/R/testFootnotesTable.R +++ /dev/null @@ -1,68 +0,0 @@ -testFootnotesTableFunc <- function(jaspResults, dataset, options) -{ - tableOfOptions(jaspResults, options, dataset) - - jaspResults[['table0']] <- createFootnotesTable(options=options) - jaspResults[['table1']] <- createFootnotesTable(options=options, notUnique=TRUE) - - createEmptiesTable(jaspResults) - createCombinations(jaspResults) -} - -createCombinations <- function(jaspResults) -{ - binary <- data.frame(list(a=c("00", "01"), b=c("10", "11"))) - rownames(binary) <- c('c', 'd') - tab <- createJaspTable("Combining row- with col-names or not!") - - tab$setData(binary) - - tab$addFootnote(message="nothing!") - tab$addFootnote(message="just row", rowNames='c') - tab$addFootnote(message="just col", colNames='a') - tab$addFootnote(message="row&&col", rowNames='d', colNames='b') - - jaspResults[["cartesianFootnotes"]] <- tab -} - -createEmptiesTable <- function(jaspResults) -{ - #for https://github.com/jasp-stats/INTERNAL-jasp/issues/538 (footnotes mess) - - tab <- createJaspTable(paste0("Table with Footnotes at empty cell")) - jaspResults[["tableEmptyCell"]] <- tab - - tab$setExpectedSize(rows=3, cols=3) - tab$setColumnName(1, 'b') - tab$setRowName(1, 'b') - - tab$setColumnName(2, 'a') - tab$setColumnName(3, 'c') - tab$setRowName(3, 'a') - - tab$addFootnote(message="hallo", rowNames='a', colNames='c') - - tab$addRows(list(a=NA, b=NA, c=NA)) - tab$addFootnote(message="NA!", rowNames='b', colNames='b') -} - -createFootnotesTable <- function(numFootnotes=3, rowNames=c("b", "c", "c"), colNames=c("a", "a", "b"), options, notUnique=FALSE) -{ - foo <- sapply(letters[21:23], function(col) paste(letters[24:26], col)) - rownames(foo) <- colnames(foo) <- letters[1:3] - - tableFootnotes <- createJaspTable(paste0("Table with Footnotes at rowNames=(", paste0(rowNames, collapse=", "), ") and colNames=(", paste0(colNames, collapse=", "), ")", collapse=""), foo, dependencies="checkbox_0") - - msgNum <- 0 - - while(msgNum < numFootnotes) - { - msgNum <- msgNum + 1 - - if(options$checkbox_0 == notUnique) tableFootnotes$addFootnote(message = paste0("msg", as.character(msgNum)), rowNames = rowNames[[msgNum]], colNames = colNames[[msgNum]]) - else if(options$checkbox_1 != notUnique) tableFootnotes$addFootnote(message = "Identical Twins!", rowNames = rowNames[[msgNum]], colNames = colNames[[msgNum]]) - else tableFootnotes$addFootnote(message = "Identical Symbols!", rowNames = rowNames[[msgNum]], colNames = colNames[[msgNum]], symbol="?") - } - - return(tableFootnotes); -} diff --git a/R/testHtml.R b/R/testHtml.R index 6c438a3..90227f7 100644 --- a/R/testHtml.R +++ b/R/testHtml.R @@ -1,4 +1,4 @@ -testHtmlFunc <- function(jaspResults, dataset, options) +testHtmlFuncInternal <- function(jaspResults, dataset, options) { tableOfOptions(jaspResults, options, dataset) diff --git a/R/testHtmlFuncWrapper.R b/R/testHtmlFuncWrapper.R new file mode 100644 index 0000000..f31ce71 --- /dev/null +++ b/R/testHtmlFuncWrapper.R @@ -0,0 +1,45 @@ +# +# Copyright (C) 2013-2025 University of Amsterdam +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +# This is a generated file. Don't change it! + +#' Test jaspHtml +#' +testHtmlFunc <- function( + data = NULL, + version = "1", + checkbox_error = FALSE, + plotHeight = 320, + plotWidth = 480) { + + defaultArgCalls <- formals(jaspTestModule::testHtmlFunc) + defaultArgs <- lapply(defaultArgCalls, eval) + options <- as.list(match.call())[-1L] + options <- lapply(options, eval) + defaults <- setdiff(names(defaultArgs), names(options)) + options[defaults] <- defaultArgs[defaults] + options[["data"]] <- NULL + options[["version"]] <- NULL + + + if (!jaspBase::jaspResultsCalledFromJasp() && !is.null(data)) { + jaspBase::storeDataSet(data) + } + + + return(jaspBase::runWrappedAnalysis("jaspTestModule", "testHtmlFunc", "testHtml.qml", options, version, FALSE)) +} \ No newline at end of file diff --git a/R/testInfo.R b/R/testInfo.R index 3d36542..531b51a 100644 --- a/R/testInfo.R +++ b/R/testInfo.R @@ -8,4 +8,4 @@ testInfoFunc <- function(jaspResults, dataset, options) jaspResults[['tafel']] <- createJaspTable(title="Glory", colTitles=c('hallo', 'wereld'), info="some info on a table") jaspResults[['nested']] <- createJaspContainer("Chairs", info='this is a container you know') jaspResults[['nested']][['egg']] <- createJaspHtml(text="This is nested within a container", info="some info on a nested jaspHtml") -} \ No newline at end of file +} diff --git a/R/testInputField.R b/R/testInputField.R index a6ad27f..77ea0d2 100644 --- a/R/testInputField.R +++ b/R/testInputField.R @@ -1,5 +1,5 @@ -testInputFieldFunc <- function(jaspResults, dataset, options) +testInputFieldFuncInternal <- function(jaspResults, dataset, options) { tableOfOptions(jaspResults, options, dataset) diff --git a/R/testInputFieldFuncWrapper.R b/R/testInputFieldFuncWrapper.R new file mode 100644 index 0000000..19ff8ca --- /dev/null +++ b/R/testInputFieldFuncWrapper.R @@ -0,0 +1,54 @@ +# +# Copyright (C) 2013-2025 University of Amsterdam +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +# This is a generated file. Don't change it! + +#' Test Input Field +#' +testInputFieldFunc <- function( + data = NULL, + version = "1", + doubleInfinity = inf, + doubleMinInfinity = -inf, + doubleWith1Decimals = 0.1, + doubleWith3Decimals = 0.123, + doubleWith5Decimals = 0.12345, + formulaHalf = "3/2", + formulaInfinity = inf, + formulaMinInfinity = -inf, + formulaNan = nan, + integerInfinity = 2, + plotHeight = 320, + plotWidth = 480) { + + defaultArgCalls <- formals(jaspTestModule::testInputFieldFunc) + defaultArgs <- lapply(defaultArgCalls, eval) + options <- as.list(match.call())[-1L] + options <- lapply(options, eval) + defaults <- setdiff(names(defaultArgs), names(options)) + options[defaults] <- defaultArgs[defaults] + options[["data"]] <- NULL + options[["version"]] <- NULL + + + if (!jaspBase::jaspResultsCalledFromJasp() && !is.null(data)) { + jaspBase::storeDataSet(data) + } + + + return(jaspBase::runWrappedAnalysis("jaspTestModule", "testInputFieldFunc", "testInputField.qml", options, version, FALSE)) +} \ No newline at end of file diff --git a/R/testJaspReport.R b/R/testJaspReport.R index 380db0d..350148f 100644 --- a/R/testJaspReport.R +++ b/R/testJaspReport.R @@ -1,5 +1,5 @@ #' @export -JaspReportTest <- function(jaspResults, dataset, options) +JaspReportTestInternal <- function(jaspResults, dataset, options) { tableOfOptions(jaspResults, options, dataset) @@ -17,4 +17,4 @@ JaspReportTest <- function(jaspResults, dataset, options) reportOnThis, dependencies=c("reportMe", "selectedVariable")) } -} \ No newline at end of file +} diff --git a/R/testLabel.R b/R/testLabel.R index fcb611a..fdd56f0 100644 --- a/R/testLabel.R +++ b/R/testLabel.R @@ -1,4 +1,4 @@ -testLabelFormatFunc <- function(jaspResults, dataset, options) +testLabelFormatFuncInternal <- function(jaspResults, dataset, options) { tableOfOptions(jaspResults, options, dataset) } diff --git a/R/testLabelFormatFuncWrapper.R b/R/testLabelFormatFuncWrapper.R new file mode 100644 index 0000000..d1f8475 --- /dev/null +++ b/R/testLabelFormatFuncWrapper.R @@ -0,0 +1,55 @@ +# +# Copyright (C) 2013-2025 University of Amsterdam +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +# This is a generated file. Don't change it! + +#' Test label format +#' +testLabelFormatFunc <- function( + data = NULL, + version = "1", + assigned = list(types = list(), value = list()), + checkbox = FALSE, + doubleFieldWithAfterLabel = 0, + dropDown = "high", + integerFieldWithAfterLabel = 0, + labelText = "Test H₀! Ttest", + plotHeight = 320, + plotWidth = 480, + radiobuttongroup = "rButton2", + textFormat = "2") { + + defaultArgCalls <- formals(jaspTestModule::testLabelFormatFunc) + defaultArgs <- lapply(defaultArgCalls, eval) + options <- as.list(match.call())[-1L] + options <- lapply(options, eval) + defaults <- setdiff(names(defaultArgs), names(options)) + options[defaults] <- defaultArgs[defaults] + options[["data"]] <- NULL + options[["version"]] <- NULL + + + if (!jaspBase::jaspResultsCalledFromJasp() && !is.null(data)) { + jaspBase::storeDataSet(data) + } + + optionsWithFormula <- c("assigned", "dropDown", "textFormat") + for (name in optionsWithFormula) { + if ((name %in% optionsWithFormula) && inherits(options[[name]], "formula")) options[[name]] = jaspBase::jaspFormula(options[[name]], data) } + + return(jaspBase::runWrappedAnalysis("jaspTestModule", "testLabelFormatFunc", "testLabelFormat.qml", options, version, FALSE)) +} \ No newline at end of file diff --git a/R/testLatexFuncWrapper.R b/R/testLatexFuncWrapper.R new file mode 100644 index 0000000..7068979 --- /dev/null +++ b/R/testLatexFuncWrapper.R @@ -0,0 +1,45 @@ +# +# Copyright (C) 2013-2025 University of Amsterdam +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +# This is a generated file. Don't change it! + +#' Test latex +#' +testLatexFunc <- function( + data = NULL, + version = "1", + checkbox_error = FALSE, + plotHeight = 320, + plotWidth = 480) { + + defaultArgCalls <- formals(jaspTestModule::testLatexFunc) + defaultArgs <- lapply(defaultArgCalls, eval) + options <- as.list(match.call())[-1L] + options <- lapply(options, eval) + defaults <- setdiff(names(defaultArgs), names(options)) + options[defaults] <- defaultArgs[defaults] + options[["data"]] <- NULL + options[["version"]] <- NULL + + + if (!jaspBase::jaspResultsCalledFromJasp() && !is.null(data)) { + jaspBase::storeDataSet(data) + } + + + return(jaspBase::runWrappedAnalysis("jaspTestModule", "testLatexFunc", "testHtml.qml", options, version, FALSE)) +} \ No newline at end of file diff --git a/R/testMinMax.R b/R/testMinMax.R index c18a916..065aeb2 100644 --- a/R/testMinMax.R +++ b/R/testMinMax.R @@ -1,5 +1,5 @@ -minMaxFunc <- function(jaspResults, dataset, options) +minMaxFuncInternal <- function(jaspResults, dataset, options) { tableOfOptions(jaspResults, options, dataset) diff --git a/R/testMixedColumnsTable.R b/R/testMixedColumnsTable.R index 2dc884c..a587b3c 100644 --- a/R/testMixedColumnsTable.R +++ b/R/testMixedColumnsTable.R @@ -1,4 +1,4 @@ -testMixedColumnsTableFunc <- function(jaspResults, dataset, options) { +testMixedColumnsTableFuncInternal <- function(jaspResults, dataset, options) { if (options[["checkbox_0"]]) set.seed(123) diff --git a/R/testMixedColumnsTableFuncWrapper.R b/R/testMixedColumnsTableFuncWrapper.R new file mode 100644 index 0000000..f72ed4b --- /dev/null +++ b/R/testMixedColumnsTableFuncWrapper.R @@ -0,0 +1,46 @@ +# +# Copyright (C) 2013-2025 University of Amsterdam +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +# This is a generated file. Don't change it! + +#' Test Mixed Columns jaspTable +#' +testMixedColumnsTableFunc <- function( + data = NULL, + version = "1", + checkbox_0 = FALSE, + checkbox_1 = FALSE, + plotHeight = 320, + plotWidth = 480) { + + defaultArgCalls <- formals(jaspTestModule::testMixedColumnsTableFunc) + defaultArgs <- lapply(defaultArgCalls, eval) + options <- as.list(match.call())[-1L] + options <- lapply(options, eval) + defaults <- setdiff(names(defaultArgs), names(options)) + options[defaults] <- defaultArgs[defaults] + options[["data"]] <- NULL + options[["version"]] <- NULL + + + if (!jaspBase::jaspResultsCalledFromJasp() && !is.null(data)) { + jaspBase::storeDataSet(data) + } + + + return(jaspBase::runWrappedAnalysis("jaspTestModule", "testMixedColumnsTableFunc", "testMixedColumns.qml", options, version, FALSE)) +} \ No newline at end of file diff --git a/R/testOptionContainsValue.R b/R/testOptionContainsValue.R index d4404c8..f26a5b5 100644 --- a/R/testOptionContainsValue.R +++ b/R/testOptionContainsValue.R @@ -1,4 +1,4 @@ -testOptionContainsValue <- function(jaspResults, dataset, options) +testOptionContainsValueInternal <- function(jaspResults, dataset, options) { tableOfOptions(jaspResults, options, dataset) diff --git a/R/testOptionContainsValueWrapper.R b/R/testOptionContainsValueWrapper.R new file mode 100644 index 0000000..bba909f --- /dev/null +++ b/R/testOptionContainsValueWrapper.R @@ -0,0 +1,49 @@ +# +# Copyright (C) 2013-2025 University of Amsterdam +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +# This is a generated file. Don't change it! + +#' Test optionContainsValue +#' +testOptionContainsValue <- function( + data = NULL, + version = "1", + irrelevantCheckBox = FALSE, + plotHeight = 320, + plotWidth = 480, + restrictedModels = list(list(modelName = "Model 1", something = FALSE))) { + + defaultArgCalls <- formals(jaspTestModule::testOptionContainsValue) + defaultArgs <- lapply(defaultArgCalls, eval) + options <- as.list(match.call())[-1L] + options <- lapply(options, eval) + defaults <- setdiff(names(defaultArgs), names(options)) + options[defaults] <- defaultArgs[defaults] + options[["data"]] <- NULL + options[["version"]] <- NULL + + + if (!jaspBase::jaspResultsCalledFromJasp() && !is.null(data)) { + jaspBase::storeDataSet(data) + } + + optionsWithFormula <- c("restrictedModels") + for (name in optionsWithFormula) { + if ((name %in% optionsWithFormula) && inherits(options[[name]], "formula")) options[[name]] = jaspBase::jaspFormula(options[[name]], data) } + + return(jaspBase::runWrappedAnalysis("jaspTestModule", "testOptionContainsValue", "testOptionContainsValue.qml", options, version, FALSE)) +} \ No newline at end of file diff --git a/R/testOptionNames.R b/R/testOptionNames.R index 299b5cc..b783c22 100644 --- a/R/testOptionNames.R +++ b/R/testOptionNames.R @@ -1,5 +1,5 @@ -testOptionNames <- function(jaspResults, dataset, options) +testOptionNamesInternal <- function(jaspResults, dataset, options) { tableOfOptions(jaspResults, options, dataset) diff --git a/R/testOptionNamesWrapper.R b/R/testOptionNamesWrapper.R new file mode 100644 index 0000000..8745969 --- /dev/null +++ b/R/testOptionNamesWrapper.R @@ -0,0 +1,55 @@ +# +# Copyright (C) 2013-2025 University of Amsterdam +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +# This is a generated file. Don't change it! + +#' Test Option Names +#' +testOptionNames <- function( + data = NULL, + version = "1", + allVariables = FALSE, + formula = FALSE, + myRadioButton = "jhsdf7634(*&(&))", + noformula = FALSE, + one = FALSE, + plotHeight = 320, + plotWidth = 480, + test = FALSE, + two = FALSE, + variables = list(types = list(), value = list())) { + + defaultArgCalls <- formals(jaspTestModule::testOptionNames) + defaultArgs <- lapply(defaultArgCalls, eval) + options <- as.list(match.call())[-1L] + options <- lapply(options, eval) + defaults <- setdiff(names(defaultArgs), names(options)) + options[defaults] <- defaultArgs[defaults] + options[["data"]] <- NULL + options[["version"]] <- NULL + + + if (!jaspBase::jaspResultsCalledFromJasp() && !is.null(data)) { + jaspBase::storeDataSet(data) + } + + optionsWithFormula <- c("variables") + for (name in optionsWithFormula) { + if ((name %in% optionsWithFormula) && inherits(options[[name]], "formula")) options[[name]] = jaspBase::jaspFormula(options[[name]], data) } + + return(jaspBase::runWrappedAnalysis("jaspTestModule", "testOptionNames", "testOptionNames.qml", options, version, FALSE)) +} \ No newline at end of file diff --git a/R/testRSource.R b/R/testRSource.R index 8aa08fa..5b7d657 100644 --- a/R/testRSource.R +++ b/R/testRSource.R @@ -1,4 +1,4 @@ -testRSourceFunc <- function(jaspResults, dataset, options) +testRSourceFuncInternal <- function(jaspResults, dataset, options) { tableOfOptions(jaspResults, options, dataset) diff --git a/R/testRSourceFuncWrapper.R b/R/testRSourceFuncWrapper.R new file mode 100644 index 0000000..8cad80e --- /dev/null +++ b/R/testRSourceFuncWrapper.R @@ -0,0 +1,53 @@ +# +# Copyright (C) 2013-2025 University of Amsterdam +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +# This is a generated file. Don't change it! + +#' Test R Source +#' +testRSourceFunc <- function( + data = NULL, + version = "1", + ComponentsList = list(), + ComponentsList2 = list(), + assignMe = list(types = list(), value = list()), + assignMe2 = list(types = list(), value = list()), + drop = "one", + drop2 = "one", + plotHeight = 320, + plotWidth = 480) { + + defaultArgCalls <- formals(jaspTestModule::testRSourceFunc) + defaultArgs <- lapply(defaultArgCalls, eval) + options <- as.list(match.call())[-1L] + options <- lapply(options, eval) + defaults <- setdiff(names(defaultArgs), names(options)) + options[defaults] <- defaultArgs[defaults] + options[["data"]] <- NULL + options[["version"]] <- NULL + + + if (!jaspBase::jaspResultsCalledFromJasp() && !is.null(data)) { + jaspBase::storeDataSet(data) + } + + optionsWithFormula <- c("ComponentsList", "ComponentsList2", "assignMe", "assignMe2", "drop", "drop2") + for (name in optionsWithFormula) { + if ((name %in% optionsWithFormula) && inherits(options[[name]], "formula")) options[[name]] = jaspBase::jaspFormula(options[[name]], data) } + + return(jaspBase::runWrappedAnalysis("jaspTestModule", "testRSourceFunc", "testRSource.qml", options, version, FALSE)) +} \ No newline at end of file diff --git a/R/testRadioButtons.R b/R/testRadioButtons.R index 80918da..b6a8c33 100644 --- a/R/testRadioButtons.R +++ b/R/testRadioButtons.R @@ -1,4 +1,4 @@ -radioButtonFunc <- function(jaspResults, dataset, options) +radioButtonFuncInternal <- function(jaspResults, dataset, options) { tableOfOptions(jaspResults, options, dataset) diff --git a/R/testReqModule.R b/R/testReqModule.R index f2eacee..4c7f0b9 100644 --- a/R/testReqModule.R +++ b/R/testReqModule.R @@ -1,6 +1,6 @@ -descriptivesLink <- function(jaspResults, dataset, options) +descriptivesLinkInternal <- function(jaspResults, dataset, options) { tableOfOptions(jaspResults, options, dataset) Descriptives::Descriptives(jaspResults, dataset, options) -} \ No newline at end of file +} diff --git a/R/testSaveLoadButton.R b/R/testSaveLoadButton.R index 172f483..09f67e2 100644 --- a/R/testSaveLoadButton.R +++ b/R/testSaveLoadButton.R @@ -1,4 +1,4 @@ -testSaveLoadButton <- function(jaspResults, dataset, options) +testSaveLoadButtonInternal <- function(jaspResults, dataset, options) { tableOfOptions(jaspResults, options, dataset) diff --git a/R/testSaveLoadButtonWrapper.R b/R/testSaveLoadButtonWrapper.R new file mode 100644 index 0000000..1785c10 --- /dev/null +++ b/R/testSaveLoadButtonWrapper.R @@ -0,0 +1,47 @@ +# +# Copyright (C) 2013-2025 University of Amsterdam +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +# This is a generated file. Don't change it! + +#' Save Load Button +#' +testSaveLoadButton <- function( + data = NULL, + version = "1", + loadMultiple = "", + loadThis = "", + plotHeight = 320, + plotWidth = 480, + saveHere = "") { + + defaultArgCalls <- formals(jaspTestModule::testSaveLoadButton) + defaultArgs <- lapply(defaultArgCalls, eval) + options <- as.list(match.call())[-1L] + options <- lapply(options, eval) + defaults <- setdiff(names(defaultArgs), names(options)) + options[defaults] <- defaultArgs[defaults] + options[["data"]] <- NULL + options[["version"]] <- NULL + + + if (!jaspBase::jaspResultsCalledFromJasp() && !is.null(data)) { + jaspBase::storeDataSet(data) + } + + + return(jaspBase::runWrappedAnalysis("jaspTestModule", "testSaveLoadButton", "testSaveLoadButton.qml", options, version, FALSE)) +} \ No newline at end of file diff --git a/R/testSpecialSources.R b/R/testSpecialSources.R index 88df641..6f95112 100644 --- a/R/testSpecialSources.R +++ b/R/testSpecialSources.R @@ -1,4 +1,4 @@ -testSpecialSourcesFunc <- function(jaspResults, dataset, options) +testSpecialSourcesFuncInternal <- function(jaspResults, dataset, options) { tableOfOptions(jaspResults, options, dataset) diff --git a/R/testSpecialSourcesFuncWrapper.R b/R/testSpecialSourcesFuncWrapper.R new file mode 100644 index 0000000..ae9f7d0 --- /dev/null +++ b/R/testSpecialSourcesFuncWrapper.R @@ -0,0 +1,66 @@ +# +# Copyright (C) 2013-2025 University of Amsterdam +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +# This is a generated file. Don't change it! + +#' Test Sources with special attributes +#' +testSpecialSourcesFunc <- function( + data = NULL, + version = "1", + assigned = list(types = list(), value = list()), + combinationCross = list(), + combinations2way = list(), + combinations3way = list(), + compList = list(list(dropDownValuesa = list(types = "unknown", value = ""), value = "#")), + controlVariables = "", + labelsFor1Variable = "", + labelsForSeveralVariable = "", + ordinalAndNominalVariables = list(types = "unknown", value = ""), + ordinalVariables = list(types = "unknown", value = ""), + plotHeight = 320, + plotWidth = 480, + scaleVariables = list(types = "unknown", value = ""), + selectedVariables = list(types = list(), value = list()), + selectedVariablesWithControls = list(optionKey = "variable", types = list(), value = list()), + selectedVariablesWithControls2 = list(optionKey = "variable", types = list(), value = list()), + severalVariablesWithLabels = list(types = list(), value = list()), + variableWithLabels = list(types = list(), value = ""), + variables = list(types = list(), value = list()), + variablesWithConditions = list(types = list(), value = list()), + variablesWithConditions2 = list(types = list(), value = list())) { + + defaultArgCalls <- formals(jaspTestModule::testSpecialSourcesFunc) + defaultArgs <- lapply(defaultArgCalls, eval) + options <- as.list(match.call())[-1L] + options <- lapply(options, eval) + defaults <- setdiff(names(defaultArgs), names(options)) + options[defaults] <- defaultArgs[defaults] + options[["data"]] <- NULL + options[["version"]] <- NULL + + + if (!jaspBase::jaspResultsCalledFromJasp() && !is.null(data)) { + jaspBase::storeDataSet(data) + } + + optionsWithFormula <- c("assigned", "combinationCross", "combinations2way", "combinations3way", "compList", "controlVariables", "labelsFor1Variable", "labelsForSeveralVariable", "ordinalAndNominalVariables", "ordinalVariables", "scaleVariables", "selectedVariables", "selectedVariablesWithControls", "selectedVariablesWithControls2", "severalVariablesWithLabels", "variableWithLabels", "variables", "variablesWithConditions", "variablesWithConditions2") + for (name in optionsWithFormula) { + if ((name %in% optionsWithFormula) && inherits(options[[name]], "formula")) options[[name]] = jaspBase::jaspFormula(options[[name]], data) } + + return(jaspBase::runWrappedAnalysis("jaspTestModule", "testSpecialSourcesFunc", "testSpecialSources.qml", options, version, FALSE)) +} \ No newline at end of file diff --git a/R/testState.R b/R/testState.R index 6b48c79..34e004b 100644 --- a/R/testState.R +++ b/R/testState.R @@ -1,4 +1,4 @@ -testStateFunc <- function(jaspResults, dataset, options) { +testStateFuncInternal <- function(jaspResults, dataset, options) { tableOfOptions(jaspResults, options, dataset) diff --git a/R/testStateFuncWrapper.R b/R/testStateFuncWrapper.R new file mode 100644 index 0000000..42b2944 --- /dev/null +++ b/R/testStateFuncWrapper.R @@ -0,0 +1,53 @@ +# +# Copyright (C) 2013-2025 University of Amsterdam +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +# This is a generated file. Don't change it! + +#' Test jaspState +#' +testStateFunc <- function( + data = NULL, + version = "1", + checkbox_0 = FALSE, + checkbox_1 = FALSE, + plotHeight = 320, + plotWidth = 480, + saveMe = "Blablabla", + tabview = list(list(checkbox_2 = FALSE, tabviewOptions = "Tab 1", variablesForStateTab2 = list(types = list(), value = list()), variablesTab1 = list(types = list(), value = list()))), + variables = list(types = list(), value = list()), + variablesForState = list(types = list(), value = list())) { + + defaultArgCalls <- formals(jaspTestModule::testStateFunc) + defaultArgs <- lapply(defaultArgCalls, eval) + options <- as.list(match.call())[-1L] + options <- lapply(options, eval) + defaults <- setdiff(names(defaultArgs), names(options)) + options[defaults] <- defaultArgs[defaults] + options[["data"]] <- NULL + options[["version"]] <- NULL + + + if (!jaspBase::jaspResultsCalledFromJasp() && !is.null(data)) { + jaspBase::storeDataSet(data) + } + + optionsWithFormula <- c("tabview", "variables", "variablesForState") + for (name in optionsWithFormula) { + if ((name %in% optionsWithFormula) && inherits(options[[name]], "formula")) options[[name]] = jaspBase::jaspFormula(options[[name]], data) } + + return(jaspBase::runWrappedAnalysis("jaspTestModule", "testStateFunc", "testState.qml", options, version, FALSE)) +} \ No newline at end of file diff --git a/R/testTabView.R b/R/testTabView.R index 312e25f..9052e82 100644 --- a/R/testTabView.R +++ b/R/testTabView.R @@ -1,5 +1,5 @@ -testTabView <- function(jaspResults, dataset, options) +testTabViewInternal <- function(jaspResults, dataset, options) { tableOfOptions(jaspResults, options, dataset) diff --git a/R/testTabViewWrapper.R b/R/testTabViewWrapper.R new file mode 100644 index 0000000..c12ef39 --- /dev/null +++ b/R/testTabViewWrapper.R @@ -0,0 +1,54 @@ +# +# Copyright (C) 2013-2025 University of Amsterdam +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +# This is a generated file. Don't change it! + +#' Test Tab View +#' +testTabView <- function( + data = NULL, + version = "1", + oneValue = TRUE, + plotHeight = 320, + plotWidth = 480, + subTabs = list(list(keyLabel = "One", keyValue = "oneValue")), + tabViewWithFactors = list(list(factors = "", levels = list(types = "unknown", value = ""), repeatedMeasuresCells = list("", ""), repeatedMeasuresFactors = list(list(levels = list("Level 1", "Level 2"), name = "RM Factor 1")), value = "New tab")), + tabs = list(list(inputType = "paths", value = "New tab")), + threeValue = FALSE, + twoValue = FALSE, + variables = list(optionKey = "variable", types = list(), value = list())) { + + defaultArgCalls <- formals(jaspTestModule::testTabView) + defaultArgs <- lapply(defaultArgCalls, eval) + options <- as.list(match.call())[-1L] + options <- lapply(options, eval) + defaults <- setdiff(names(defaultArgs), names(options)) + options[defaults] <- defaultArgs[defaults] + options[["data"]] <- NULL + options[["version"]] <- NULL + + + if (!jaspBase::jaspResultsCalledFromJasp() && !is.null(data)) { + jaspBase::storeDataSet(data) + } + + optionsWithFormula <- c("subTabs", "tabViewWithFactors", "tabs", "variables") + for (name in optionsWithFormula) { + if ((name %in% optionsWithFormula) && inherits(options[[name]], "formula")) options[[name]] = jaspBase::jaspFormula(options[[name]], data) } + + return(jaspBase::runWrappedAnalysis("jaspTestModule", "testTabView", "testTabView.qml", options, version, FALSE)) +} \ No newline at end of file diff --git a/R/testTableFormatting.R b/R/testTableFormatting.R index 747a46a..2bdddf2 100644 --- a/R/testTableFormatting.R +++ b/R/testTableFormatting.R @@ -1,4 +1,4 @@ -testTableFormattingFunc <- function(jaspResults, dataset, options) +testTableFormattingFuncInternal <- function(jaspResults, dataset, options) { tableOfOptions(jaspResults, options, dataset) diff --git a/R/testTableFormattingFuncWrapper.R b/R/testTableFormattingFuncWrapper.R new file mode 100644 index 0000000..43917aa --- /dev/null +++ b/R/testTableFormattingFuncWrapper.R @@ -0,0 +1,45 @@ +# +# Copyright (C) 2013-2025 University of Amsterdam +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +# This is a generated file. Don't change it! + +#' Test jaspTable formatting +#' +testTableFormattingFunc <- function( + data = NULL, + version = "1", + checkbox_error = FALSE, + plotHeight = 320, + plotWidth = 480) { + + defaultArgCalls <- formals(jaspTestModule::testTableFormattingFunc) + defaultArgs <- lapply(defaultArgCalls, eval) + options <- as.list(match.call())[-1L] + options <- lapply(options, eval) + defaults <- setdiff(names(defaultArgs), names(options)) + options[defaults] <- defaultArgs[defaults] + options[["data"]] <- NULL + options[["version"]] <- NULL + + + if (!jaspBase::jaspResultsCalledFromJasp() && !is.null(data)) { + jaspBase::storeDataSet(data) + } + + + return(jaspBase::runWrappedAnalysis("jaspTestModule", "testTableFormattingFunc", "testTableFormatting.qml", options, version, FALSE)) +} \ No newline at end of file diff --git a/R/testTableView.R b/R/testTableView.R index ad8ad5d..e393532 100644 --- a/R/testTableView.R +++ b/R/testTableView.R @@ -1,4 +1,4 @@ -testTableView <- function(jaspResults, dataset, options) { +testTableViewInternal <- function(jaspResults, dataset, options) { tableOfOptions(jaspResults, options, dataset) diff --git a/R/testTableViewWrapper.R b/R/testTableViewWrapper.R new file mode 100644 index 0000000..35e1b43 --- /dev/null +++ b/R/testTableViewWrapper.R @@ -0,0 +1,53 @@ +# +# Copyright (C) 2013-2025 University of Amsterdam +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +# This is a generated file. Don't change it! + +#' Test Table View +#' +testTableView <- function( + data = NULL, + version = "1", + diagonalTable = list(list(levels = list(), name = "data 1", values = list())), + minimumLevels = 2, + numberOfRows = 2, + numberOfRows2 = 1, + plotHeight = 320, + plotWidth = 480, + testButtons = list(list(levels = "Row 0", name = "data 1", values = "A"), list(levels = "Row 0", name = "data 2", values = "a"), list(levels = "Row 0", name = "data 3", values = "a")), + testTypes = list(list(levels = list("Row 0", "Row 1"), name = "data 1", values = list("A", "B")), list(levels = list("Row 0", "Row 1"), name = "data 2", values = list("2", "2")), list(levels = list("Row 0", "Row 1"), name = "data 3", values = list(4, 4)))) { + + defaultArgCalls <- formals(jaspTestModule::testTableView) + defaultArgs <- lapply(defaultArgCalls, eval) + options <- as.list(match.call())[-1L] + options <- lapply(options, eval) + defaults <- setdiff(names(defaultArgs), names(options)) + options[defaults] <- defaultArgs[defaults] + options[["data"]] <- NULL + options[["version"]] <- NULL + + + if (!jaspBase::jaspResultsCalledFromJasp() && !is.null(data)) { + jaspBase::storeDataSet(data) + } + + optionsWithFormula <- c("diagonalTable", "testButtons", "testTypes") + for (name in optionsWithFormula) { + if ((name %in% optionsWithFormula) && inherits(options[[name]], "formula")) options[[name]] = jaspBase::jaspFormula(options[[name]], data) } + + return(jaspBase::runWrappedAnalysis("jaspTestModule", "testTableView", "testTableView.qml", options, version, FALSE)) +} \ No newline at end of file diff --git a/R/testTextArea.R b/R/testTextArea.R index f5590a8..edfc0e0 100644 --- a/R/testTextArea.R +++ b/R/testTextArea.R @@ -1,5 +1,5 @@ -testTextArea <- function(jaspResults, dataset, options) +testTextAreaInternal <- function(jaspResults, dataset, options) { tableOfOptions(jaspResults, options, dataset) diff --git a/R/testTextAreaWrapper.R b/R/testTextAreaWrapper.R new file mode 100644 index 0000000..4ca80e6 --- /dev/null +++ b/R/testTextAreaWrapper.R @@ -0,0 +1,51 @@ +# +# Copyright (C) 2013-2025 University of Amsterdam +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +# This is a generated file. Don't change it! + +#' Test TextArea +#' +testTextArea <- function( + data = NULL, + version = "1", + dropDown = list(types = "unknown", value = ""), + lavaanScript = list(columns = list(), model = "", modelOriginal = ""), + plotHeight = 320, + plotWidth = 480, + tabView = list(list(checkBoxesInTabView = list(), lavaanCodeInTabView = list(columns = list(), model = "", modelOriginal = ""), usedVariablesInTabView = list(optionKey = "variable", types = list(), value = list()), value = "Model 1")), + usedVariables = list(optionKey = "variable", types = list(), value = list())) { + + defaultArgCalls <- formals(jaspTestModule::testTextArea) + defaultArgs <- lapply(defaultArgCalls, eval) + options <- as.list(match.call())[-1L] + options <- lapply(options, eval) + defaults <- setdiff(names(defaultArgs), names(options)) + options[defaults] <- defaultArgs[defaults] + options[["data"]] <- NULL + options[["version"]] <- NULL + + + if (!jaspBase::jaspResultsCalledFromJasp() && !is.null(data)) { + jaspBase::storeDataSet(data) + } + + optionsWithFormula <- c("dropDown", "lavaanScript", "tabView", "usedVariables") + for (name in optionsWithFormula) { + if ((name %in% optionsWithFormula) && inherits(options[[name]], "formula")) options[[name]] = jaspBase::jaspFormula(options[[name]], data) } + + return(jaspBase::runWrappedAnalysis("jaspTestModule", "testTextArea", "testTextArea.qml", options, version, FALSE)) +} \ No newline at end of file diff --git a/R/testTransposedTable.R b/R/testTransposedTable.R deleted file mode 100644 index f92ad83..0000000 --- a/R/testTransposedTable.R +++ /dev/null @@ -1,54 +0,0 @@ -testTransposedTableFunc <- function(jaspResults, dataset, options) -{ - tableOfOptions(jaspResults, options, dataset) - - - # the correlation table stored here - mainTable <- createJaspTable(title = "Transposed Table") - mainTable$showSpecifiedColumnsOnly <- TRUE - - # I guess we want the transpose - mainTable$transpose <- TRUE - - # but we don't want overtitles over the "displayed" columns - #mainTable$transposeWithOvertitle <- FALSE - - # the displayed columns are supposed to be just the number of variable - mainTable$addColumnInfo(name = "var", title = "", type = "string", overtitle="Variables") - - # now we need to define the "displayed" rows; ideally defining columns with overtitles - whichtests <- c(TRUE, TRUE, TRUE)#options$pearson, options$spearman, options$kendallsTauB) - testsTitles <- c("Pearson's rho", "Spearman's r", "Kendall's Tau B")[whichtests] - tests <- c("pearson", "spearman", "kendall")[whichtests] - - vars <- options$variables - vvars <- vars # inside just, this will be actually .v(vars) - - for (vi in seq_along(vvars)){ - for(ti in seq_along(tests)){ - mainTable$addColumnInfo( - name = paste(vvars[vi], tests[ti], "r", sep = "."), - title = testsTitles[ti], type = "number", - overtitle = vars[vi]) # this overtitle should appear as a first column - - - #if(options$reportSignificance) - mainTable$addColumnInfo(name = paste(vvars[vi], tests[ti], "p.value", sep = "."), - title = "p-value", type = "pvalue", - overtitle = vars[vi]) # this overtitle should appear as a first column - - # here will be more option-dependent stuff, but this should be enough as an example - } - } - - # the displayed columns will show the variable names - mainTable$addColumns(list(var = vars)) - - # everything else is passed to the table with the computed results... - # fake example - mainTable$addColumns(list(contNormal.pearson.r = c(0.2, 0.3, 0.4), contNormal.pearson.p.value = c(0.1, 0.034, 0.001))) - - #mainTable$print() - jaspResults[['transposeMe']] <- mainTable - -} diff --git a/R/testVariablesForm.R b/R/testVariablesForm.R index 4a7afb2..16c96b7 100644 --- a/R/testVariablesForm.R +++ b/R/testVariablesForm.R @@ -1,8 +1,8 @@ -testVariablesForm <- function(jaspResults, dataset, options) +testVariablesFormInternal <- function(jaspResults, dataset, options) { tableOfOptions(jaspResults, options, dataset) jaspResults[["Hallo"]] <- createJaspHtml(text="hallo!") -} \ No newline at end of file +} diff --git a/R/testVariablesFormWrapper.R b/R/testVariablesFormWrapper.R new file mode 100644 index 0000000..4182236 --- /dev/null +++ b/R/testVariablesFormWrapper.R @@ -0,0 +1,62 @@ +# +# Copyright (C) 2013-2025 University of Amsterdam +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +# This is a generated file. Don't change it! + +#' Test Variables Form +#' +testVariablesForm <- function( + data = NULL, + version = "1", + addCheckBox = FALSE, + addDropDown = FALSE, + maxLevel = -1, + maxNumLevel = -1, + minLevel = -1, + minNumLevel = -1, + nominal = FALSE, + ordinal = FALSE, + plotHeight = 320, + plotWidth = 480, + scale = TRUE, + setType = list(types = "unknown", value = ""), + toggleRemoveInvisibles = FALSE, + toggleVisibility = TRUE, + varS = list(types = list(), value = list()), + varToggle = list(types = list(), value = list()), + variablesWithControls = list(optionKey = "variable", types = list(), value = list())) { + + defaultArgCalls <- formals(jaspTestModule::testVariablesForm) + defaultArgs <- lapply(defaultArgCalls, eval) + options <- as.list(match.call())[-1L] + options <- lapply(options, eval) + defaults <- setdiff(names(defaultArgs), names(options)) + options[defaults] <- defaultArgs[defaults] + options[["data"]] <- NULL + options[["version"]] <- NULL + + + if (!jaspBase::jaspResultsCalledFromJasp() && !is.null(data)) { + jaspBase::storeDataSet(data) + } + + optionsWithFormula <- c("setType", "varS", "varToggle", "variablesWithControls") + for (name in optionsWithFormula) { + if ((name %in% optionsWithFormula) && inherits(options[[name]], "formula")) options[[name]] = jaspBase::jaspFormula(options[[name]], data) } + + return(jaspBase::runWrappedAnalysis("jaspTestModule", "testVariablesForm", "testVariablesForm.qml", options, version, FALSE)) +} \ No newline at end of file diff --git a/R/tooltipFunc.R b/R/tooltipFunc.R index da224da..f40f3f1 100644 --- a/R/tooltipFunc.R +++ b/R/tooltipFunc.R @@ -1,4 +1,4 @@ -tooltipFunc <- function(jaspResults, dataset, options) +tooltipFuncInternal <- function(jaspResults, dataset, options) { tableOfOptions(jaspResults, options, dataset) jaspResults[["Hallo"]] <- createJaspHtml(text="hallo!") diff --git a/R/tooltipFuncWrapper.R b/R/tooltipFuncWrapper.R new file mode 100644 index 0000000..bb9c090 --- /dev/null +++ b/R/tooltipFuncWrapper.R @@ -0,0 +1,107 @@ +# +# Copyright (C) 2013-2025 University of Amsterdam +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +# This is a generated file. Don't change it! + +#' ToolTip +#' +#' @param IntegerWithInfo, Field with info +#' @param IntegerWithTooLongValueAndInfo, Field with info and too long value +#' @param checkBoxWithInfo, Great info +#' Defaults to \code{FALSE}. +#' @param checkBoxWithInfoAndTooltip, Great info +#' Defaults to \code{FALSE}. +#' @param checkBoxWithSubCheckBox, Info for checkbox with sub checkboxes +#' Defaults to \code{FALSE}. +#' @param dropDownLabelAbove, Dropdown with info and label above +#' @param dropDownLabelAboveWithExtraInfo, Dropdown with extra info for value 'Two' and label above +#' \itemize{ +#' \item \code{"one"} (default) +#' \item \code{"two"}: Extra info for Two +#' \item \code{"three"} +#' } +#' @param dropdownWithExtraInfo, Dropdown with info +#' \itemize{ +#' \item \code{"one"} (default) : This is one +#' \item \code{"two"} +#' } +#' @param dropdownWithInfo, Dropdown with info +#' @param radioButtonsWithInfo, Info for the RadioButton group +#' \itemize{ +#' \item \code{"radioWithoutInfo"} +#' \item \code{"radioWithInfo"}: Info just for this RadioButton +#' } +#' @param rowRadioButtonsWithInfo, Info for the Row RadioButton group +#' \itemize{ +#' \item \code{"radioWithoutInfo"} +#' \item \code{"radioWithInfo"}: Info just for this RadioButton +#' } +#' @param rowRadioButtonsWithoutLabelWithInfo, No label but an info +#' \itemize{ +#' \item \code{"one"} +#' \item \code{"two"} +#' } +#' @param subCheckBoxWithInfo, Info for Sub CheckBox +#' Defaults to \code{FALSE}. +tooltipFunc <- function( + data = NULL, + version = "1", + IntegerWithInfo = 0, + IntegerWithTooLongValue = 1234567, + IntegerWithTooLongValueAndInfo = 1234567, + RadioButtonGroup1 = "group1RadioButton1", + RadioButtonGroup2 = "group2RadioButton1", + checkBoxWithInfo = FALSE, + checkBoxWithInfoAndTooltip = FALSE, + checkBoxWithSubCheckBox = FALSE, + checkBoxWithTooltip = FALSE, + checkBoxWithoutTootip = FALSE, + dropDownLabelAbove = "one", + dropDownLabelAboveWithExtraInfo = "one", + dropdownWithExtraInfo = "one", + dropdownWithInfo = "one", + plotHeight = 320, + plotWidth = 480, + radioButtons = "radioWithoutInfo", + radioButtonsWithInfo = "radioWithoutInfo", + rowRadioButtons = "radioWithInfo", + rowRadioButtonsWithInfo = "radioWithoutInfo", + rowRadioButtonsWithoutLabelWithInfo = "one", + rowRadioButtonsWithoutLabelWithoutInfo = "two", + subCheckBoxWithInfo = FALSE, + subCheckBoxWithoutInfo = FALSE) { + + defaultArgCalls <- formals(jaspTestModule::tooltipFunc) + defaultArgs <- lapply(defaultArgCalls, eval) + options <- as.list(match.call())[-1L] + options <- lapply(options, eval) + defaults <- setdiff(names(defaultArgs), names(options)) + options[defaults] <- defaultArgs[defaults] + options[["data"]] <- NULL + options[["version"]] <- NULL + + + if (!jaspBase::jaspResultsCalledFromJasp() && !is.null(data)) { + jaspBase::storeDataSet(data) + } + + optionsWithFormula <- c("dropDownLabelAbove", "dropDownLabelAboveWithExtraInfo", "dropdownWithExtraInfo", "dropdownWithInfo") + for (name in optionsWithFormula) { + if ((name %in% optionsWithFormula) && inherits(options[[name]], "formula")) options[[name]] = jaspBase::jaspFormula(options[[name]], data) } + + return(jaspBase::runWrappedAnalysis("jaspTestModule", "tooltipFunc", "Tooltip.qml", options, version, FALSE)) +} \ No newline at end of file diff --git a/TODO-multidataset.md b/TODO-multidataset.md new file mode 100644 index 0000000..d9b4beb --- /dev/null +++ b/TODO-multidataset.md @@ -0,0 +1,149 @@ +# Multi-dataset aware feature — status (2026-10-08, post FilterSelect refactor) +> +> **SUPERSEDED (2026-10-18, mds-plan D9.1).** This is a historical snapshot of the pre-plan +> (2026-10-08) FilterSelect-refactor era. Several mechanisms it names were replaced by the +> mds-plan implementation (the typed per-term provenance, per-filter encoder views, the one +> global strict decode table, generated send-time `.meta`): `JASPControl::effectiveVarInfo()` +> / `effectiveSelectionFilter()` / `varInfoForItem()` (walk deleted, D4.1 — successors: +> `resolvedSelectionHost()`/`resolvedVarInfo()`), `createMeta` stamps (generated send-time +> `.meta` + the transient `.filterIds` sidecar, C8/C11), `.sliceKeyForDataSet` (retired with +> the zero-export datasets contract), and "jaspTools handouts keyed by dataset id" (stale — +> both engine runs and jaspTools handouts key the datasets list by the resolved filter id). +> The current module-author contract lives in `R/multiDataSet.R`; the "Done" section below is +> the commit log of that era and is kept verbatim as history. + +## Decided design (ground truth, v2 — supersedes the linked-dropdown v1) +- Options and dataset slices share the ENCODED namespace (`JASPColumn___Encoded`); + R indexes the slice's columns by option value directly; results/plots are decoded on the way + out (engine sendString loops every workspace encoder; plotly decode loops referenced datasets). +- **Selection state is per-HOST, not per-analysis.** Any component consuming variable info for + itself declares `dataSetSelectionOption` (a `HostFilterSelection` member): VariablesFormBase + and TextAreaBase do so today. The option's value is a **FILTER ID** (a filter implies its + dataset; ids survive renames), written in BOTH modes by one reusable `FilterSelect.qml` + (a bound DropDown, `values:` = workspace filter list "DataSet - Filter", created behind + `Loader { active: selectionAvailable }` so an unnamed bound control can never exist). +- **Provider chain follows the component chain:** each selecting host owns a `VariableInfo` + whose provider is the selected `Filter` (Filter *is* a VariableInfoProvider: per-dataset + columns/types/levels, rows filtered, `Filter::columnEncoder()` gives the dataset encoder). + `JASPControl::effectiveVarInfo()` / `effectiveSelectionFilter()` resolve nearest-selecting-host + (self included), else `AnalysisForm::varInfo()` (analysis filter / shownFilter — today's + behavior for everything non-aware). Non-JASPControl pieces (Formula) use the static + `JASPControl::varInfoForItem()`. +- **Selection code never touches** `analysis->setFilterId` / `workspace->setShownDataSet` — + those belong to the analysis-GLOBAL FilterMenuButton (the true legacy widget, untouched). +- `.meta` provenance is stamped with the filter the value was selected THROUGH (host's, else + analysis'), so per-form datasets survive save/restore and reach the engine per control. +- **Slice queue keyed by filter id:** `prepareMultiDataSetRun` collects every distinct + (dataSetId, filterId) from `.meta`; one slice per pair (two forms on one dataset with + different filters → two differently-filtered dataframes). R-side `datasets` keys = resolved + filter id, `attr "dataSetIds"`/`"dataSetNames"` map back; a selection option indexes its own + slice directly: `datasets[[as.character(options$dataSetA)]]`. jaspTools handouts stay keyed by + dataset id; `.sliceKeyForDataSet` bridges both. +- **Syntax deviation confined to the wrapper layer:** bridge translates a selection option that + names a loaded dataset ("haha") into that DataSet's default filter id before binding (unknown + names fail loudly with the list of loaded datasets; "" too). `datasets = list(haha = df, ...)` + stays the user-facing API (names become DataSet titles). Forms are mode-blind. +- Single-dataset API on AnalysisBase **refuses** for aware analyses instead of silently answering + about one arbitrary dataset: `Analysis::dataSet()`, `isColumnFreeOrMine()`, + `isOwnComputedColumn()`, computed-column request handlers (log + safe default); + `usesDataSet()` consults `referencedDataSets()` when aware. +- `preloadData` forced true for aware analyses; engine checks multiAware BEFORE preload. +- Vanished filter/dataset while selected: selection falls back to the analysis/current filter + (same spirit as the analysis-global behavior), never dangles (HostFilterSelection validates on + dataSetRemoved/filtersCountChanged). + +## jaspTools BRIDGE PARITY — DONE 2026-10-09 (plan: /home/virtuoos/.opencode/plan/jaspTools-bridge-parity.md) +jaspTools::runAnalysis now runs the real SyntaxInterface bridge (hard dep on jaspSyntax): +data loaded as genuine DataSets, options validated+encoded by the QML form (auto_unbox +serialization!), filter-keyed slice queue, results decoded on exit via +jaspSyntax::decodeJsonText. The R emulation (rbridge.R), encodedDataset and the generator's +regex encoder family are DELETED — generated tests use plain names end to end. +testMultiDataSet.R now asserts plain-name parity (this file's paths 1&3 were rewritten to it): +- path 1: aware run through the bridge — real dataset ids, per-form selection honoured, + slices trimmed engine-style to the referenced columns (wanted-cols: Group+Score / Age+Score), + everything decoded again on exit; +- path 3: single-dataset legacy semantics run through testEncodingFunc — the multiDataSetNonAware + twin was removed (mds-plan C4/M8, its wrapper always errored); an AWARE analysis + ALWAYS gets the slice queue now (correct engine semantics), and the bridge (like syntax mode) + rejects empty selection options rather than silently coercing them. +Fixed on the way (were pre-existing branch bugs): segfault when an unnamed loadDataSet +destroyed the workspace under cached QML forms (jasp-desktop 476b630dc); ColumnEncoderContext +now carries the encode prefix so context replay survives dataset changes (jasp-desktop +9fc29eb59). Known pre-existing, NOT chased: jaspSyntax test-desktop-jasp-contract +readDatasetFromJaspFile hits a DataSet::dbLoad id assertion in the bridge subprocess (fails +identically without the parity work); testDbMigration `:memory:` (older news). + +## Review round 2026-10-08 (subagent review + user directives) - RESOLVED +- CRITICAL empty-GUI-tables: module tables were filled row-by-row with `:`-containing keys + (poisoned DOM ids -> JS showed nothing though the data was there). Fixed: data.frame + setData + (row-major, no custom ids) per user directive. jasp-desktop 841431fae, jaspBase 73ab.., module. +- CRITICAL save/reload: provenance side table is now per (dataset,filter) PAIR by name, and + isFilterSelection-marked option VALUES get remapped on load (unit-tested incl. numeric + collision guard). Real file save/reopen test still open (item 2). +- Mirroring removed (user: mirroring is bad; jaspTools-legacy is the ONLY exception): _encoder() + duplication hoisted to BoundControlTextArea, dead variableInfoProvider deleted, selection-key + logic centralized in jaspBase. +- Slices are indexed by FILTER everywhere; dataset-id helper routing ERRORS on multi-slice + datasets (never silently first); getSliceKey/getSlice/sliceDataSetId/sliceTitle are the + writer-facing API; encoding stays per dataset (names embed ds id). +- Also: TextAreaBase options now translated by the bridge (optionNames scan), queue dedups + equal slice keys + exposes "primary" slice (attr primarySliceKey), VariableInfo emits on + first provider, plot decode no longer trips the aware guard. + +## TODO +1. [verify-manual] GUI smoke test (last resort, everything else is scripted): two CSVs, open + Test Multi-DataSet, flip BOTH "Data" dropdowns independently, watch tables stay filled and + unlinked; R/JAGS text areas with an option show their own dropdown. +2. [open] Saved-file reload of an aware analysis end-to-end: per-form filter-id selections + restored through `bindTo` (+ `restoreProvenanceFromBoundValues`), vanished filter → fallback. + No automated test yet (needs RPC save/open or a crafted .jasp fixture). +3. [open] Same-dataset-two-filters desktop GUI demo: the module analysis routes per form's + selection slice; a test data pair where BOTH forms use dataset 1 with different filters would + exercise the C8 two-slice path in the GUI (covered in rlogic unit, not e2e). +4. [upstream] JASP bug candidate: bundle tooling writes manifest `"version":[[0,96,2]]` but + `InstalledModules::parseModuleInfo` throws on arrays → module silently skipped at restart. + installModuleLocal.sh works around it (rewrites to string). +5. [KNOWN PRE-EXISTING FAILURE — user-confirmed, NOT from the FilterSelect refactor] + `ctest testDbMigration` aborts: "Trying to load ':memory:' but it doesn't exist!" — broken + since the earlier multi-dataset work (workspace/database lifecycle). Investigate separately. +6. [note] Syntax-mode side effects to watch: readRequestedDataset() after an aware parse consumes + queue slices; jaspSyntax result decoding is per current dataset — multi-dataset result decoding + in pure syntax output may need the same multi-encoder care. +7. [ops] jaspSyntax installs MUST pass `JASPSYNTAX_LIB_PATH=`; + after each SyntaxInterface rebuild copy the .so to BOTH `jaspSyntax/src/` and + `.toolLib/jaspSyntax/libs/`. +8. [later] FormulaBase as host in its own right (its own FilterSelect + option): today a Formula + only follows the VariablesForm it lives in (varInfoForItem). Top-level Formulas keep the + analysis-wide fallback. + +## Done (FilterSelect refactor, 2026-10-08) — plan file: /home/virtuoos/.opencode/plan/multidataset-filterselect.md +- jasp-desktop `8636529f5` RED contract tests first (two forms two filters simultaneously, + bogus-id rejection, per-form list contents; fixture dataset "Second" w/ overlapping TestInts). +- jasp-desktop `557f089d1` the core: HostFilterSelection (filter-id, per-host state, C10 + validation), JASPControl::effectiveVarInfo/effectiveSelectionFilter + all consumers rerouted + (lists, SourceItem, textareas, custom contrasts, filteredDataEntry, R/JAGS textareas), + FilterSelect.qml, createMeta stamps host filter, Filter::columnEncoder, single-dataset guards, + usesDataSet-referencedDataSets; DELETED: dataSetSelection bool + both old dropdowns, + selectDataSetByName/applyDataSetSelection, _setInitialized re-apply hook, name-based + selection, analysis-filter mirroring. +- jasp-desktop `3bbafd477` + jaspBase `69ec48c` filter-keyed slice queue (pair collector, + per-pair slices, `.sliceKeyForDataSet`, rlogic + unit tests for the contract). +- jasp-desktop `e8a4db4e5` bridge name→default-filter-id translation (C7), errors list datasets. +- jasp-desktop `46c4603c3` TextAreaBase host (FilterSelect in TextArea.qml, JAGS highlighter + follows host) + Formula follows its form's host. +- module `c2d59eb` qmls use only dataSetSelectionOption (+depends, now resolvable in both modes); + R routes via the form's own selection slice with legacy fallback; wrapper regenerates. +- module RPC verifier now drives the dropdown contract itself (options carry filter ids, NO + analysis_setFilter anywhere): independent simultaneous selections, A:Score mean 12.50 vs + B:Score mean 17.73 (same name, different datasets — the "compare with itself" trap made + impossible), per-form .meta stamps (1/1 vs 2/2), n=10 everywhere, zero raw JASPColumn_*. +- Suites green: JASPTest 110, JASPQuickTest 96 (T1-T3c/T9/T12), JASPTestColumnEncoderContext 10, + rlogic incl. bridge round-trip, module paths 1-3, RPC desktop e2e ALL CHECKS PASSED. + (ctest except testDbMigration — see item 5, pre-existing.) + +## Crash 2026-10-08 (fixed, historical) +SEGV opening Test Multi-DataSet: `depends: "dataSetA"` targeted the syntax-only dropdown that was +deliberately unnamed on desktop → `_addExplicitDependency` inserted nullptr → `sortControls` +deref'd it. Interim fix `785afe486` (skip unresolved deps — kept as defense). The FilterSelect +refactor removed the ROOT cause: the dropdown is now named and registered in both modes, and +`Loader { active: selectionAvailable }` makes an unnamed bound control structurally impossible. diff --git a/Tools/buildBundle.R b/Tools/buildBundle.R new file mode 100644 index 0000000..ae8ea66 --- /dev/null +++ b/Tools/buildBundle.R @@ -0,0 +1,379 @@ +#!/usr/bin/env Rscript +# Build an installable .JASPModule bundle of jaspTestModule from this checkout. +# +# The jaspBase record in renv.lock is temporarily swapped for a Local record pointing at the +# jasp-desktop checkout (default: ../jasp-desktop/Engine/jaspBase), so whatever is checked out +# there (e.g. branch multiDataSetAware) is compiled and packed into the bundle. Everything is +# built from source into a private renv library under ModuleBundleBuildDir/ - no JASP cellar +# involved, so it works on a rolling-release system (Arch) with the R you have. +# +# Requires a "tool library": a private R library (NOT your user library) holding jaspModuleTools, +# jaspModuleBundleManager, renv and their dependencies - plus jaspSyntax for the build-time QML +# smoke check (optional but recommended). Default location: ../.toolLib, sibling of this checkout; +# override with JASP_MODULE_TOOLLIB. Run this script without one and it prints the exact setup +# commands with your real paths filled in. +# +# Usage (from the jaspTestModule directory, or anywhere): +# Rscript Tools/buildBundle.R # bundle with local jaspBase +# Rscript Tools/buildBundle.R --update-lockfile # re-resolve renv.lock first (network) +# Rscript Tools/buildBundle.R --remote-jaspbase # use the jaspBase from renv.lock (GitHub) +# Rscript Tools/buildBundle.R --jasp-version=development +# Rscript Tools/buildBundle.R --check-only # pre-flight checks only, no build +# Rscript Tools/buildBundle.R --no-install-local # skip the local developer-JASP install +# Rscript Tools/buildBundle.R --help # this usage text +# +# Result: bundles_out/jaspTestModule.JASPModule (+ manifest json), installable via +# JASP: + (add modules) menu -> Add module -> From file. + +args <- commandArgs(trailingOnly = TRUE) + +scriptPath <- normalizePath(sub("^--file=", "", grep("^--file=", commandArgs(FALSE), value = TRUE)[[1]])) +moduledir <- normalizePath(file.path(dirname(scriptPath), "..")) +rootdir <- normalizePath(file.path(moduledir, "..")) + +jaspBaseDir <- Sys.getenv("JASP_BASE_DIR", file.path(rootdir, "jasp-desktop", "Engine", "jaspBase")) +toolLib <- Sys.getenv("JASP_MODULE_TOOLLIB", file.path(rootdir, ".toolLib")) +workdir <- file.path(moduledir, "ModuleBundleBuildDir") +resultdir <- file.path(moduledir, "bundles_out") + +localJaspBase <- !("--remote-jaspbase" %in% args) +updateLockfile <- "--update-lockfile" %in% args +checkOnly <- "--check-only" %in% args + +jaspVersion <- "development" +for (a in args) if (grepl("^--jasp-version=", a)) jaspVersion <- sub("^--jasp-version=", "", a) + +gitBranchOf <- function(dir) { + branch <- tryCatch(suppressWarnings(system2("git", c("-C", dir, "rev-parse", "--abbrev-ref", "HEAD"), + stdout = TRUE, stderr = TRUE)), + error = function(e) character(0)) + if (!length(branch) || any(grepl("fatal|not a git", branch))) "unknown" else branch[1] +} + +# The single source of tool-library setup truth: printed by the header claim above, by the +# missing-directory pre-flight and by the missing-package pre-flight, always with the real +# paths of THIS checkout filled in. +printToolLibSetup <- function() { + bundleManagerDir <- file.path(rootdir, "jasp-desktop", "Engine", "jaspModuleBundleManager") + moduleToolsDir <- file.path(rootdir, "jaspModuleTools") + jaspSyntaxDir <- file.path(rootdir, "jaspSyntax") + + cat("\n", + "This script builds with a private \"tool library\": a plain R library directory holding\n", + "jaspModuleTools, jaspModuleBundleManager, renv and their dependencies - plus jaspSyntax\n", + "for the build-time QML smoke check. It never touches your user library.\n\n", + "Expected location: ", toolLib, "\n", + "(keep it elsewhere? set the JASP_MODULE_TOOLLIB environment variable)\n\n", + "One-time setup - paste into a shell (adjust the first line to where you want the library):\n\n", + sep = "") + cat(sprintf(" toolLib=%s\n mkdir -p \"$toolLib\"\n\n", toolLib)) + cat(paste0( + " Rscript -e '.libPaths(c(toolLib, .libPaths()))\n", + " install.packages(c(\"renv\",\"fs\",\"pkgdepends\",\"usethis\",\"withr\",\"curl\",\"rjson\",\"stringr\",\n", + " \"archive\",\"openssl\",\"jsonlite\",\"remotes\"), lib=toolLib,\n", + " repos=\"https://cloud.r-project.org\")'\n\n")) + if (dir.exists(bundleManagerDir)) + cat(paste0( + " Rscript -e '.libPaths(c(toolLib, .libPaths()))\n", + " remotes::install_local(\"", bundleManagerDir, "\", lib=toolLib,\n", + " upgrade=\"never\", dependencies=FALSE)'\n\n")) + else + cat(paste0( + " # jaspModuleBundleManager lives in the jasp-desktop checkout (Engine/jaspModuleBundleManager);\n", + " # not found at ", bundleManagerDir, " - point the path at your jasp-desktop checkout:\n", + " Rscript -e '.libPaths(c(toolLib, .libPaths()))\n", + " remotes::install_local(\"/Engine/jaspModuleBundleManager\", lib=toolLib,\n", + " upgrade=\"never\", dependencies=FALSE)'\n\n")) + if (dir.exists(moduleToolsDir)) + cat(paste0( + " Rscript -e '.libPaths(c(toolLib, .libPaths()))\n", + " remotes::install_local(\"", moduleToolsDir, "\", lib=toolLib,\n", + " upgrade=\"never\", dependencies=FALSE)'\n\n")) + else + cat(paste0( + " # jaspModuleTools - no checkout next to this module; clone it first:\n", + " # git clone https://github.com/jasp-stats/jaspModuleTools\n", + " Rscript -e '.libPaths(c(toolLib, .libPaths()))\n", + " remotes::install_local(\"\", lib=toolLib,\n", + " upgrade=\"never\", dependencies=FALSE)'\n\n")) + if (dir.exists(jaspSyntaxDir)) + cat(paste0( + " # optional but recommended: jaspSyntax (QML smoke check; the test scripts need it too)\n", + " Rscript -e '.libPaths(c(toolLib, .libPaths()))\n", + " remotes::install_local(\"", jaspSyntaxDir, "\", lib=toolLib, upgrade=\"never\")'\n")) + else + cat(paste0( + " # optional but recommended: jaspSyntax (QML smoke check; the test scripts need it too)\n", + " # git clone https://github.com/jasp-stats/jaspSyntax\n", + " Rscript -e '.libPaths(c(toolLib, .libPaths()))\n", + " remotes::install_local(\"\", lib=toolLib, upgrade=\"never\")'\n")) + cat(paste0( + " # (its configure fetches a prebuilt libSyntaxInterface from a GitHub Release; to use a local\n", + " # jasp-desktop build instead, prefix the install with:\n", + " # JASPSYNTAX_LIB_PATH=/SyntaxInterface/libSyntaxInterface.so)\n\n")) +} + +if ("--help" %in% args || "-h" %in% args) { + cat("\nUsage: Rscript Tools/buildBundle.R [options]\n\n") + cat("Builds bundles_out/jaspTestModule.JASPModule from this checkout. By default the renv.lock\n", + "jaspBase record is swapped for the local jasp-desktop checkout (--remote-jaspbase keeps the\n", + "GitHub record instead). Tool packages come from the tool library (see the header).\n\n", sep = "") + cat(" --update-lockfile re-resolve renv.lock with pkgdepends first (needs network)\n") + cat(" --remote-jaspbase keep the renv.lock jaspBase record (GitHub) instead of the local checkout\n") + cat(" --jasp-version= buildforJaspVersion for jaspModuleTools::compile (default: development)\n") + cat(" --check-only run the pre-flight checks and exit without building\n") + cat(" --no-install-local skip installing the bundle into the local developer JASP\n") + cat(" --help this text\n\n") + quit(status = 0) +} + +# ---- pre-flight: fail early with actionable output, before touching the filesystem ---------- + +if (!dir.exists(toolLib)) { + cat("\nTool library not found at ", toolLib, ".\n", sep = "") + printToolLibSetup() + quit(status = 1) +} + +.libPaths(c(toolLib, .libPaths())) + +missingPkgs <- Filter(function(p) !requireNamespace(p, quietly = TRUE), + c("renv", "fs", "pkgdepends", "jsonlite", "archive", "jaspModuleTools")) +if (length(missingPkgs)) { + cat("\nCould not load (tool library ", toolLib, " is first on the path): ", + paste(missingPkgs, collapse = ", "), "\n", + "Install the missing ones into the tool library (not into your user library):\n", sep = "") + printToolLibSetup() + quit(status = 1) +} + +if (localJaspBase && !checkOnly && !dir.exists(jaspBaseDir)) + stop("No jaspBase checkout at ", jaspBaseDir, " (set JASP_BASE_DIR or use --remote-jaspbase).") + +lockPath <- file.path(moduledir, "renv.lock") +if (!file.exists(lockPath)) + stop("No renv.lock at ", lockPath, " - is ", moduledir, " the jaspTestModule checkout?") + +message("Pre-flight: + module : ", moduledir, " (branch ", gitBranchOf(moduledir), ") + jaspBase : ", if (localJaspBase) paste0(jaspBaseDir, " (branch ", gitBranchOf(jaspBaseDir), ", local swap)") else "from renv.lock (GitHub) via --remote-jaspbase", " + tool library : ", toolLib, " + workdir : ", workdir, " + bundle output: ", resultdir, " + jasp version : ", jaspVersion, + if (checkOnly) "\n check-only: pre-flight passed, not building.") + +if (checkOnly) + quit(status = 0) + +# ---- tool environment ------------------------------------------------------------------------ + +suppressPackageStartupMessages({ library(jsonlite) }) +library(jaspModuleTools) + +# No renv sandbox: binary R distributions (Arch!) have no R sources to link it against. +Sys.setenv(RENV_CONFIG_SANDBOX_ENABLED = "false") +options(jaspRemoteCellarRedownload = FALSE) + +restoreLock <- function() { + if (!is.null(lockBackup)) { + writeLines(lockBackup, lockPath) + lockBackup <<- NULL + message("restored original ", lockPath) + } +} +lockBackup <- NULL + +if (updateLockfile) { + message("re-resolving ", lockPath, " with pkgdepends ...") + jaspModuleTools::updateLockfile(moduledir) + lockBackup <- readLines(lockPath) # keep the freshly resolved one; never lose it +} + +# ---- the renv sandbox that renv cannot build here (see the comment) --------------------------- + +dir.create(workdir, recursive = TRUE, showWarnings = FALSE) + +# Binary R distributions (Arch!) have no R sources, so renv's sandbox can't be built - but +# jaspModuleTools copies packages that renv::restore skips because they live in the system +# library (recommended ones like codetools) out of it. Fake it: the sandbox is nothing but a +# symlink farm over the system library, so build exactly that at the path renv resolves +# (RENV_PATHS_SANDBOX plus its linux-arch-rolling/R-4.6/ suffix). +Sys.setenv(RENV_PATHS_SANDBOX = file.path(workdir, "sandbox")) +fakeSandbox <- renv:::renv_paths_sandbox() +dir.create(fakeSandbox, recursive = TRUE, showWarnings = FALSE) +for (lib in unique(c(R.home("library"), .libPaths()))) + for (pkg in list.dirs(lib, recursive = FALSE, full.names = FALSE)) + if (!file.exists(file.path(fakeSandbox, pkg)) && nzchar(pkg) && !grepl("^00LOCK", pkg)) + file.symlink(file.path(lib, pkg), file.path(fakeSandbox, pkg)) + +# ---- swap the renv.lock jaspBase record for the local checkout -------------------------------- + +if (localJaspBase) { + baseDesc <- read.dcf(file.path(jaspBaseDir, "DESCRIPTION"))[1, ] + message("swapping jaspBase in renv.lock for local checkout: ", jaspBaseDir, + " (v", baseDesc[["Version"]], ", branch ", gitBranchOf(jaspBaseDir), ")") + + lockBackup <- readLines(lockPath) + lock <- jsonlite::fromJSON(lockPath, simplifyVector = FALSE) + + depsFields <- baseDesc[c("Depends", "Imports")] + depsFields <- depsFields[!is.na(depsFields)] + requires <- unique(trimws(unlist(strsplit(paste(depsFields, collapse = ","), ",")))) + requires <- gsub("\\s*\\(.*", "", requires) # "Rcpp (>= 0.12.14)" -> "Rcpp" + requires <- setdiff(requires, c("", "R")) + + # Same local-record shape jaspModuleTools uses for the module itself (createLocalRecord). + lock$Packages$jaspBase <- list( + Package = "jaspBase", + Version = baseDesc[["Version"]], + Source = "Local", + Requirements = requires, + RemoteType = "local", + RemoteUrl = normalizePath(jaspBaseDir), + Cacheable = TRUE, + Hash = unname(rlang::hash(rlang::hash_file(fs::dir_ls(jaspBaseDir, type = "file", recurse = TRUE)))) + ) + + writeLines(jsonlite::toJSON(lock, auto_unbox = TRUE, pretty = 2), lockPath) +} + +dir.create(resultdir, showWarnings = FALSE) + +# renv::restore skips same-version records, so a changed local jaspBase checkout would silently +# NOT be rebuilt into the pkglib (the version stays 0.20.4). When bundling against the local +# checkout: drop the installed jaspBase first so it is always rebuilt from source. +if (localJaspBase) { + pkglib <- file.path(workdir, "build_dir", basename(moduledir)) + installedBase <- file.path(pkglib, "jaspBase") + if (dir.exists(installedBase)) { + message("removing stale jaspBase from ", pkglib, " (will rebuild from the local checkout)") + unlink(installedBase, recursive = TRUE) + } +} + +# ---- compile + pack the bundle ---------------------------------------------------------------- + +message("compiling + bundling (log is chatty, first run compiles all of jaspBase's deps) ...") +tryCatch(jaspModuleTools::compile( + moduledir = moduledir, + workdir = file.path(workdir, "build_dir"), + resultdir = resultdir, + createBundle = TRUE, + bundleAll = TRUE, + buildforJaspVersion = jaspVersion, + useJASPRemoteCellar = FALSE, + localizeJASPModules = "identicalToLockfile" # our lockfile (incl. the swapped jaspBase) is what we want +), error = function(e) { + message("compile() failed: ", conditionMessage(e)) + calls <- sys.calls() + for (i in seq_along(calls)) message(sprintf(" #%02d %s", i, substr(paste(deparse(calls[[i]]), collapse = " "), 1, 160))) + stop(e) +}, finally = restoreLock()) + +# ---- build-time QML smoke check through the native SyntaxInterface ---------------------------- +# Load the module Description and the multi-dataset form through jaspSyntax (in the tool +# library): catches QML errors like missing imports or unknown types at build time, without +# firing up JASP. Skipped when jaspSyntax isn't installed. +Sys.setenv(QT_QPA_PLATFORM = "offscreen") +if (requireNamespace("jaspSyntax", quietly = TRUE)) { + message("checking module Description + QML forms via jaspSyntax ...") + suppressPackageStartupMessages(library(jaspSyntax)) + desc <- readModuleDescription(moduledir) + stopifnot(!is.null(desc)) + for (check in list( + list(func = "multiDataSetFunc", qml = "testMultiDataSet.qml", aware = TRUE))) { + qmlFile <- file.path(moduledir, "inst", "qml", check$qml) + stopifnot(file.exists(qmlFile)) + parseOptions <- NULL + if (check$aware) { + # The aware forms declare dataSetSelectionOptions; the bridge (rightly) demands every one + # of them names a loaded dataset - give the smoke check one dummy to point them all at. + jaspSyntax::loadDataSets(list(bundleCheck = data.frame(ok = 1))) + hits <- Filter(function(h) length(h) == 2L, + regmatches(readLines(qmlFile, warn = FALSE), + regexec('dataSetSelectionOption[[:space:]]*:[[:space:]]*"([^"]+)"', + readLines(qmlFile, warn = FALSE)))) + sels <- vapply(hits, function(h) h[[2]], character(1)) + stopifnot(length(sels) > 0L) + parseOptions <- stats::setNames(as.list(rep("bundleCheck", length(sels))), sels) + } + parsed <- jaspSyntax::parseQmlOptions(qmlFile, options = parseOptions, moduleName = "jaspTestModule", + analysisName = check$func, isolated = FALSE) + stopifnot(!is.null(parsed)) + message(" ", check$func, " -> ", check$qml, " loads OK") + } +} else { + message("NOTE: jaspSyntax not installed (see github.com/jasp-stats/jaspSyntax) - QML not checked.") +} + +# ---- verify: our jaspBase is in the bundle, not some cached GitHub build ---------------------- +bundle <- file.path(resultdir, "jaspTestModule.JASPModule") +if (!file.exists(bundle)) stop("compile() did not produce ", bundle) + +checkdir <- file.path(tempdir(), "bundleCheck") +unlink(checkdir, recursive = TRUE); dir.create(checkdir, recursive = TRUE) +archive::archive_extract(bundle, dir = checkdir) + +manifest <- jsonlite::fromJSON(file.path(checkdir, "jaspTestModule_manifest.json")) +jaspBaseHash <- sub(" .*", "", grep("=> jaspBase_", manifest$mapping, value = TRUE)) +if (length(jaspBaseHash) != 1) stop("jaspBase not in the bundle manifest (mapping: ", paste(manifest$mapping, collapse = ", "), ")") + +pkgdir <- file.path(checkdir, "lib", "jaspBase") +dir.create(pkgdir, recursive = TRUE, showWarnings = FALSE) +archive::archive_extract(file.path(checkdir, jaspBaseHash), dir = pkgdir) + +# The L0 tars are *installed* package trees: load the bundled jaspBase and look at what is +# actually in it. That proves OUR build (not some cached GitHub one) is in the bundle. +.libPaths(c(file.path(checkdir, "lib"), file.path(workdir, "build_dir", "jaspTestModule"), .libPaths())) +suppressPackageStartupMessages(library(jaspBase, lib.loc = file.path(checkdir, "lib"))) + +if (localJaspBase) { + # Zero-export contract (CTO 2026-10-09): the multi-dataset machinery ships NO accessor + # exports - analyses index datasets[[filter-id-key]] directly. Any reappearance fails here. + forbidden <- c("getSliceKey", "getSlice", "sliceDataSetId", "sliceTitle", "getDataSetFor", + "getDataSetColumn", "dataSetIdFromEncoded", "dataSetNameFromEncoded", "storeDataSets") + leaked <- intersect(forbidden, getNamespaceExports("jaspBase")) + runBody <- any(grepl("multiDataSetJson", deparse(body(jaspBase::runJaspResults)))) + if (length(leaked) || !runBody) + stop("bundled jaspBase violates the zero-export contract (leaked exports: ", + paste(leaked, collapse = ", "), if (runBody) "" else " + runJaspResults has no multiDataSetJson flow", ")") + message("VERIFIED: bundled jaspBase loads, queue flow present, zero leaked accessor exports.") +} else { + message("bundle jaspBase version: ", as.character(utils::packageVersion("jaspBase", lib.loc = file.path(checkdir, "lib")))) +} + +# ---- wrapper-body gate: every jaspBase:: reference must be exported by the bundled jaspBase ---- +# The generator once emitted the @noRd-internal jaspBase::storeDataSets into aware wrappers - a +# regenerated wrapper then died in plain R with "'storeDataSets' is not an exported object". The +# forbidden-exports check above only guards the jaspBase side; nothing caught a broken +# *reference* on the wrapper side. parse() first so comments mentioning symbols (deparse drops +# them) can never false-positive. +wrapperFiles <- list.files(file.path(moduledir, "R"), pattern = "[.]R$", full.names = TRUE) +codeNoComments <- unlist(lapply(wrapperFiles, function(f) deparse(parse(f, keep.source = FALSE))), use.names = FALSE) +refs <- unique(unlist(regmatches(codeNoComments, + gregexpr("jaspBase::(?!:)`?([A-Za-z._][A-Za-z0-9._]*)", codeNoComments, perl = TRUE)), use.names = FALSE)) +refNames <- sub("^jaspBase::`?", "", refs) +unknown <- setdiff(refNames, getNamespaceExports("jaspBase")) +if (length(unknown)) + stop("wrapper bodies reference jaspBase symbols the installed jaspBase does not export: ", + paste(unknown, collapse = ", "), " (generated by an outdated generator? regenerate the wrappers)") +message("VERIFIED: wrapper bodies reference only exported jaspBase symbols (", + length(refNames), " distinct).") + +message("Bundle: ", bundle, " (size: ", format(file.size(bundle) / 2^20, digits = 1), " MiB)") + +# ---- install into the local developer JASP ---------------------------------------------------- +# So the module is simply THERE after every build (same result as "Add module -> From file", +# through JASP's own installer - see Tools/installModuleLocal.sh for the mechanism). A failure +# here is a warning only: the bundle itself is fine (most often JASP is still running). +if (!("--no-install-local" %in% args) && !nzchar(Sys.getenv("SKIP_MODULE_LOCAL_INSTALL"))) { + installer <- file.path(moduledir, "Tools", "installModuleLocal.sh") + status <- system2(installer, shQuote(bundle)) + if (identical(as.integer(status), 0L)) + message("installed into the local JASP (module_libs + manifest + dev prefs) - restart ./build/Desktop/JASP to pick it up.") + else + warning("bundle built OK, but the local install failed (is JASP running?) - run Tools/installModuleLocal.sh manually.") +} else { + message("Install in JASP: + menu -> Add module -> From file, pick that .JASPModule (or run Tools/installModuleLocal.sh).") +} diff --git a/Tools/installModuleLocal.sh b/Tools/installModuleLocal.sh new file mode 100755 index 0000000..8f99115 --- /dev/null +++ b/Tools/installModuleLocal.sh @@ -0,0 +1,144 @@ +#!/usr/bin/env bash +# installModuleLocal.sh - (re)install the built jaspTestModule bundle into JASP's +# user module library WITHOUT the GUI "Add module -> From file" click. +# +# What it does (all via JASP's own mechanism, see references below): +# 1. Runs jaspModuleBundleManager::installJaspModuleBundle(installPath, bundlePath) - +# exactly the R call JASP's "Add module -> From file" makes +# (jasp-desktop/Desktop/modules/dynamicmodules.cpp:310-345, getJsonForBundleInstallRequest). +# It untars the bundle's L0 package archives into +# ~/.local/share/JASP/JASP/Modules/binary_pkgs//, writes +# Modules/manifests/jaspTestModule_manifest.json and creates the module's private +# R library Modules/module_libs/jaspTestModule/ -> ../../binary_pkgs/. +# On reinstall it first uninstalls the previous version (removes only binary_pkgs +# not referenced by any remaining manifest). +# 2. Rewrites the installed manifest's "version" field to a plain string ("0.96.2"). +# Reason: JASP scans Modules/manifests/jasp*.json at startup to (re)load installed +# modules (Desktop/modules/installedmodules.cpp, getAllAvailableModules -> parseModuleInfo). +# parseModuleInfo() calls version.isMember("Version") which, with the jsoncpp in this +# build, THROWS on the array format [[0,96,2]] that the bundle tooling emits +# ("Could not parse module manifest ... requires objectValue or nullValue") -> +# the module is silently skipped on every restart. The string format is one of the +# formats parseModuleInfo explicitly supports (root["version"].isString() branch). +# 3. Makes sure the two JASP preferences needed for a *dev* module are set +# (Qt QSettings INI at ~/.config/JASP/JASP.conf - there is no preferences.json): +# developerMode=true - without it getModules() erases jaspTestModule +# (installedmodules.cpp:86-87) +# modulesRemembered=...jaspTestModule... - modulesRemember defaults to true, so a +# remembered module is enabled (ribbon button shown) +# at startup (ribbonmodel.cpp, loadModules()). +# +# Usage: +# Tools/installModuleLocal.sh [path-to-bundle] [--verify] +# bundle default: /bundles_out/jaspTestModule.JASPModule +# --verify afterwards run the dev JASP headless (offscreen) and grep the log +# for proof that the module loads at startup. +# Env overrides: JASP_MODULES_DIR, JASP_CONF, JASP_MODULE_TOOLLIB, JASP_DESKTOP_DIR, JASP_BIN +# +# After this script, every "jasp-desktop/build/Desktop/JASP" start shows jaspTestModule; +# rerun it after each rebuild of the bundle. JASP must not be running. +set -euo pipefail + +MODULE_NAME=jaspTestModule +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +MODULE_DIR="$(dirname "$SCRIPT_DIR")" +ROOT_DIR="$(dirname "$MODULE_DIR")" + +BUNDLE="$MODULE_DIR/bundles_out/$MODULE_NAME.JASPModule" +VERIFY=0 +for a in "$@"; do + case "$a" in + --verify) VERIFY=1 ;; + *) BUNDLE="$a" ;; + esac +done + +MODULES_DIR="${JASP_MODULES_DIR:-$HOME/.local/share/JASP/JASP/Modules}" +CONF="${JASP_CONF:-$HOME/.config/JASP/JASP.conf}" +JASP_BIN="${JASP_BIN:-${JASP_DESKTOP_DIR:-$ROOT_DIR/jasp-desktop}/build/Desktop/JASP}" + +[ -f "$BUNDLE" ] || { echo "ERROR: bundle not found: $BUNDLE (build it with Tools/buildBundle.R)" >&2; exit 1; } +command -v Rscript >/dev/null || { echo "ERROR: Rscript not on PATH" >&2; exit 1; } +if pgrep -f "build/Desktop/JASP( |$)" >/dev/null 2>&1; then + echo "ERROR: JASP appears to be running; quit it first (install replaces module_libs/manifests)." >&2; exit 1 +fi + +# --- find a library containing jaspModuleBundleManager (+ fs/rjson/stringr/archive) -------- +find_tool_lib() { + local c + for c in "${JASP_MODULE_TOOLLIB:-}" \ + "$ROOT_DIR/jasp-desktop/build/Modules/Tools/jaspModuleBundleManager_library" \ + "$ROOT_DIR/.toolLib"; do + [ -n "$c" ] && [ -d "$c/jaspModuleBundleManager" ] && [ -d "$c/archive" ] && { echo "$c"; return; } + done + return 1 +} +TOOL_LIB="$(find_tool_lib)" || { echo "ERROR: no R library with jaspModuleBundleManager found (set JASP_MODULE_TOOLLIB)" >&2; exit 1; } +echo "==> tool lib : $TOOL_LIB" +echo "==> bundle : $BUNDLE" +echo "==> install : $MODULES_DIR" + +# --- 1. install the bundle exactly like the GUI does --------------------------------------- +Rscript --vanilla -e " + .libPaths(c('$TOOL_LIB', .libPaths())) + p <- jaspModuleBundleManager::installJaspModuleBundle( + installPath = '$MODULES_DIR', + bundlePath = '$BUNDLE') + cat('installed module lib:', as.character(p), '\n') +" + +# --- 2. normalize manifest version to the string format JASP's startup parser accepts ------ +MANIFEST="$MODULES_DIR/manifests/${MODULE_NAME}_manifest.json" +[ -f "$MANIFEST" ] || { echo "ERROR: expected manifest not written: $MANIFEST" >&2; exit 1; } +Rscript --vanilla -e " + .libPaths(c('$TOOL_LIB', .libPaths())) + m <- rjson::fromJSON(file = '$MANIFEST') + if (!is.character(m\$version)) { + v <- unlist(m\$version) + m\$version <- paste(v, collapse = '.') + writeLines(rjson::toJSON(m, indent = 1), '$MANIFEST') + cat('manifest version normalized ->', m\$version, '\n') + } else cat('manifest version already a string:', m\$version, '\n') +" + +# --- 3. preferences (Qt QSettings INI): developerMode + remembered/enabled module ---------- +if [ -f "$CONF" ]; then + cp -f "$CONF" "$CONF.bak-lastinstall" +else + mkdir -p "$(dirname "$CONF")"; printf '[General]\n' > "$CONF" +fi +awk -v mod="$MODULE_NAME" ' + BEGIN { dev=0; rem=0; remHas=0; out="" } + /^[[:space:]]*developerMode[[:space:]]*=/ { dev=1; sub(/=.*/,"=true"); print; next } + /^[[:space:]]*modulesRemembered[[:space:]]*=/ { + rem=1 + if ($0 ~ ("(^|\\|)[[:space:]]*" mod "([[:space:]]*\\||[[:space:]]*$)")) { print; remHas=1; next } + sub(/[[:space:]]*$/, "") + if ($0 ~ /=$/) print $0 mod; else print $0 "|" mod + next + } + { print } + END { + if (!dev) print "developerMode=true" + if (!rem) print "modulesRemembered=" mod + } +' "$CONF" > "$CONF.tmp" && mv "$CONF.tmp" "$CONF" +echo "==> prefs : $CONF (developerMode=true, modulesRemembered includes $MODULE_NAME)" + +echo "==> installed tree:" +ls -ld "$MODULES_DIR/module_libs/$MODULE_NAME" >/dev/null && \ + echo " $MODULES_DIR/module_libs/$MODULE_NAME ($(ls "$MODULES_DIR/module_libs/$MODULE_NAME" | wc -l) pkgs)" +grep -o '"version": *"[^"]*"' "$MANIFEST" | sed 's/^/ manifest /' + +# --- 4. optional offscreen verification ----------------------------------------------------- +if [ "$VERIFY" = 1 ]; then + [ -x "$JASP_BIN" ] || { echo "ERROR: JASP binary not found: $JASP_BIN (set JASP_BIN)" >&2; exit 1; } + LOG=/tmp/opencode/jasp_load.log; mkdir -p /tmp/opencode + echo "==> starting $JASP_BIN (offscreen, 25s) -> $LOG" + timeout 25 env QT_QPA_PLATFORM=offscreen "$JASP_BIN" --newData >"$LOG" 2>&1 || true + echo "==> proof:" + grep -in "could not parse module manifest" "$LOG" && { echo "FAIL: manifest still rejected"; exit 1; } + grep -in "jaspTestModule" "$LOG" | head -5 + grep -in "Modules::EntryBase\|RibbonButton.*is ready" "$LOG" | head -3 + echo "==> OK: no manifest parse error and $MODULE_NAME activity present in $LOG" +fi diff --git a/Tools/testMultiDataSet.R b/Tools/testMultiDataSet.R new file mode 100644 index 0000000..6a671ae --- /dev/null +++ b/Tools/testMultiDataSet.R @@ -0,0 +1,215 @@ +#!/usr/bin/env Rscript +# Full headless test of the multi-dataset aware analysis (Test Multi-DataSet). +# +# Path 1 (jaspTools::runAnalysis + datasets=, BRIDGE PARITY): plain column names and plain +# dataframes - jaspTools now runs the real SyntaxInterface bridge (loadDataSets -> QML parse: +# validation, per-form dataset selection, .meta stamping, per-dataset encoding, filter-keyed +# slice queue) and decodes the results on the way out, exactly like the engine. What the test +# asserts plain is what the author typed; the encoding roundtrip happens for real in between. +# Path 2 (wrapper, jaspTestModule::multiDataSetFunc(datasets=...)): plain names too, through +# runWrappedAnalysis -> the same bridge preparation (subprocess, no jaspTools). +# Path 3 (legacy single-dataset run through jaspTools): plain in, plain out via the bridge's +# encode -> decode roundtrip; no datasets argument so the analysis takes its old path. +# +# Run with: Rscript Tools/testMultiDataSet.R (after Tools/buildBundle.R at least once, +# which installs the patched jaspBase + this module into the build library) +# +# Override locations with JASP_TEST_PKGLIB / JASP_MODULE_TOOLLIB. + +moduledir <- normalizePath(file.path(dirname(sub("^--file=", "", grep("^--file=", commandArgs(FALSE), value = TRUE)[[1]])), "..")) +pkglib <- Sys.getenv("JASP_TEST_PKGLIB", file.path(moduledir, "ModuleBundleBuildDir", "build_dir", "jaspTestModule")) +toollib <- Sys.getenv("JASP_MODULE_TOOLLIB", normalizePath(file.path(moduledir, "..", ".toolLib"))) + +stopifnot(dir.exists(pkglib), dir.exists(toollib)) +setwd(moduledir) +.libPaths(c(pkglib, toollib, .libPaths())) + +Sys.setenv(QT_QPA_PLATFORM = "offscreen") + +failures <- 0L +check <- function(ok, label) { + cat(if (isTRUE(ok)) "PASS :" else "FAIL :", label, "\n") + if (!isTRUE(ok)) failures <<- failures + 1L +} + +# jaspTable json is transposed: data[[j]] holds one table column (schema.fields order), its cells +# keyed by per-row ids ("colN", NOT in insertion order). columnValues returns cells + their row +# keys so assertions can be order-independent. +columnValues <- function(tb, fieldName) { + if (!is.list(tb) || is.null(tb$data)) return(character(0)) + # row-major (setData(data.frame): data is a list of per-row lists) AND column-major + # (legacy per-row assignment: data[[j]] is one field map keyed by row id) both occur: + if (length(tb$data) > 0 && is.list(tb$data[[1]])) { + cells <- vapply(tb$data, function(row) as.character(row[[fieldName]] %||% NA_character_), character(1)) + return(stats::setNames(cells, paste0("row", seq_along(cells)))) + } + j <- which(vapply(tb$schema$fields, function(f) identical(f$name, fieldName), logical(1)))[1] + if (is.na(j) || length(tb$data) < j) return(character(0)) + d <- tb$data[[j]] + rowKeys <- names(d)[vapply(d, length, integer(1)) > 0L] + stats::setNames(vapply(d[rowKeys], function(v) as.character(v), character(1), USE.NAMES = FALSE), rowKeys) +} + +`%||%` <- function(x, y) if (is.null(x)) y else x + +csvA <- read.csv(file.path(moduledir, "Tools/testdata/DataSetA.csv")) # Score, Age, Group +csvB <- read.csv(file.path(moduledir, "Tools/testdata/DataSetB.csv")) # Score, Age, Condition + +# ------------------------------------------------------------------ Path 1: jaspTools bridge ---- +cat("== Path 1: jaspTools::runAnalysis through the bridge (engine semantics, plain names) ==\n") +suppressMessages(library(jaspTools)) +suppressMessages(jaspTools::setupJaspTools( + pathJaspDesktop = normalizePath(file.path(moduledir, "..", "jasp-desktop")), + installJaspModules = FALSE, installJaspCorePkgs = FALSE, quiet = TRUE, force = TRUE)) +jaspTools::setPkgOption("module.dirs", moduledir) +jaspTools::setPkgOption("reinstall.modules", FALSE) + +# exactly what a syntax-mode user writes: datasets named, each variables form says which +# dataset it draws from (dataSetA/dataSetB), columns by their plain header names. +opts1 <- list(dependentA = c("Score", "Group"), + dependentB = c("Score", "Age"), + dataSetA = "DataSetA", dataSetB = "DataSetB", + showMeta = FALSE, guardDemo = TRUE, plotWidth = 400, plotHeight = 300) +res1 <- jaspTools::runAnalysis("multiDataSetFunc", options = opts1, + datasets = list(DataSetA = csvA, DataSetB = csvB), + view = FALSE, quiet = TRUE) +r1 <- res1$results + +check(identical(res1$status, "complete"), "run 1 completed") +check(length(r1$dataSetOverview$schema$fields) == 5L, "dataset overview has 5 columns (id/title/rows/cols/columnNames)") + +ovId <- columnValues(r1$dataSetOverview, "id") +ovTitle <- columnValues(r1$dataSetOverview, "title") +ovCols <- columnValues(r1$dataSetOverview, "cols") +check(length(ovId) == 2L && all(grepl("^[0-9]+$", unname(ovId))), "overview ids are real (bridge) dataset ids") +check(setequal(unname(ovTitle), c("DataSetA", "DataSetB")), "overview titles are the names from the datasets list") +# the queue hands slices trimmed to the columns the options reference (wanted-cols), exactly +# as the engine's prepareMultiDataSetRun does - not the full csv. +check(all(ovCols == "2"), "slices arrive trimmed to the referenced columns (engine wanted-cols)") + +# the overview shows DECODED names: the analysis saw JASPColumn_* tokens, jaspTools decoded +# the whole results json on the way out (engine sendString equivalent). +ovNames <- columnValues(r1$dataSetOverview, "columnNames") +check(all(c("Group, Score", "Age, Score") %in% unname(ovNames)), + "overview column names came back decoded (encode -> decode roundtrip is real)") + +sv <- r1$selectedVariables +svSrc <- columnValues(sv, "source") +svVar <- columnValues(sv, "variable") +svId <- columnValues(sv, "dataSetId") +svName <- columnValues(sv, "dataSetName") +svN <- columnValues(sv, "n") +svValues <- columnValues(sv, "values") +check(all(unname(svVar) %in% c("Score", "Group", "Age", "Condition")), "routing table variables are plain names") + +rowFor <- function(src, var, dsTitle) which(svSrc == src & svVar == var & svName == dsTitle) +iA <- rowFor("A", "Score", "DataSetA") +iAg <- rowFor("A", "Group", "DataSetA") +iB <- rowFor("B", "Score", "DataSetB") +iBag <- rowFor("B", "Age", "DataSetB") +check(length(iA) == 1L && length(iAg) == 1L && length(iB) == 1L && length(iBag) == 1L, + "four rows routed by (form, variable, dataset) - per-form selection honoured") +check(length(c(iA, iAg, iB, iBag)) == 4L && all(svN[c(iA, iAg, iB, iBag)] == "10"), "every column computed over 10 rows") +check(length(iA) == 1L && grepl(sprintf("mean %.2f", mean(csvA$Score)), svValues[[iA]], fixed = TRUE), + "form A Score = DataSetA's Score (mean matches csv)") +check(length(iB) == 1L && grepl(sprintf("mean %.2f", mean(csvB$Score)), svValues[[iB]], fixed = TRUE), + "form B Score = DataSetB's Score (identically named column, no cross-slice mixup)") +check(any(grepl(paste(utils::head(unique(as.character(csvA$Group)), 5), collapse = ", "), svValues[iAg], fixed = TRUE)), + "form A Group found in DataSetA") +check(any(grepl(paste(utils::head(unique(as.character(csvB$Age)), 5), collapse = ", "), svValues[iBag], fixed = TRUE)), + "form B Age = DataSetB's Age (DataSetA's Age would show different levels)") +check(all(unname(svId) %in% unname(ovId)), "routing dataSetIds agree with the overview ids") + +raw1 <- jsonlite::toJSON(res1, auto_unbox = TRUE) +check(!grepl("JASPColumn_", raw1), "no encoded tokens anywhere in the returned results (decoded on exit)") +check(grepl("not available in multi-dataset aware analyses", raw1, fixed = TRUE), + "guard demo: readDataSet() hard-stopped during the aware run") + +# ------------------------------------------------------------------ Bridge surface ---- +cat("\n== Bridge boundary: loadDataSets returns ids (informational); selections are TITLES ==\n") +# the syntax surface of the dataset selection options is human strings ONLY - +# the bare TITLE for a dataset's default filter, "title - filterName" for a saved named one. +# The integer ids the loaders hand back stay useful for logging and for slicing results by, +# but writing one into a selection option is now a hard error (the old numeric-id +# acceptance let a title shadow an id and vice versa - gone). +ids <- jaspSyntax::loadDataSets(list(DataSetA = csvA, DataSetB = csvB)) +check(is.integer(ids) && length(ids) == 2L && identical(names(ids), c("DataSetA", "DataSetB")) && all(ids > 0), + "loadDataSets returns the named integer dataset ids (informational, not selection syntax)") + +qmlFile <- file.path(find.package("jaspTestModule"), "qml", "testMultiDataSet.qml") +parseSelection <- function(selA, selB) { + opts <- list(dependentA = c("Score", "Group"), dependentB = c("Score", "Age"), + dataSetA = selA, dataSetB = selB, + showMeta = FALSE, guardDemo = FALSE, plotWidth = 400, plotHeight = 300) + jaspSyntax::loadQmlAndParseOptionsStatus("jaspTestModule", "multiDataSetFunc", qmlFile, + as.character(jsonlite::toJSON(opts, auto_unbox = TRUE, digits = NA, null = "null")), "1", TRUE) +} + +stTitle <- tryCatch(parseSelection("DataSetA", "DataSetB"), error = function(e) e) +check(!inherits(stTitle, "error") && nzchar(stTitle$options), "title selections parse through the bridge") +parsed <- jsonlite::fromJSON(stTitle$options, simplifyVector = FALSE) +keyA <- as.character(utils::tail(unlist(parsed$dataSetA), 1L)) +keyB <- as.character(utils::tail(unlist(parsed$dataSetB), 1L)) +msj <- jsonlite::fromJSON(stTitle$multiDataSetJson) +check(all(c(keyA, keyB) %in% as.character(msj$ids)) && keyA != keyB, + "title selections resolved to two distinct queued slice keys (filter-id key space)") +check(identical(as.character(msj$dataSetIds[[keyA]]), as.character(ids[[1]])), + "the slice a title selected belongs to exactly that dataset") + +errId <- tryCatch({ parseSelection(as.integer(ids[[1]]), as.integer(ids[[2]])); "" }, error = function(e) conditionMessage(e)) +check(grepl("matches no dataset selection string", errId), + "integer dataset ids are rejected on the syntax surface (no numeric-id acceptance anymore)") +errBadTitle <- tryCatch({ parseSelection("NoSuchDataSet", "DataSetB"); "" }, error = function(e) conditionMessage(e)) +check(grepl("matches no dataset selection string", errBadTitle) && grepl("available:", errBadTitle), + "an unknown title is rejected loudly and the error lists what IS selectable") +errNamed <- tryCatch({ parseSelection("DataSetA - myfilter", "DataSetB"); "" }, error = function(e) conditionMessage(e)) +check(grepl("matches no dataset selection string", errNamed), + "'title - name' fails loudly in full R: freshly loaded datasets have only their default filter") +errShape <- tryCatch({ + jaspSyntax::loadQmlAndParseOptionsStatus("jaspTestModule", "multiDataSetFunc", qmlFile, + as.character(jsonlite::toJSON(list( + dependentA = c("Score", "Group"), dependentB = c("Score", "Age"), + dataSetA = list(types = "unknown", value = list("DataSetA", "more")), # array inside: auto_unbox keeps it an array + dataSetB = "DataSetB", + showMeta = FALSE, guardDemo = FALSE, plotWidth = 400, plotHeight = 300), + auto_unbox = TRUE, digits = NA, null = "null")), "1", TRUE) + "" +}, error = function(e) conditionMessage(e)) +check(grepl("pass the dataset title directly", errShape), + "a {types,value} selection with no plain value inside is rejected with the direct-title hint") +stWrapped <- tryCatch(parseSelection(list(types = "unknown", value = "DataSetA"), "DataSetB"), error = function(e) e) +check(!inherits(stWrapped, "error"), + "the desktop-shaped {types,value} with a plain title inside unwraps (old wrapper defaults stay valid)") + +# ------------------------------------------------------------------ Path 2: wrapper call ------- +# The wrapper path is run in a clean Rscript subprocess (no jaspTools runtime in the way): +cat("\n== Path 2: wrapper (datasets= named list + dataset selection options) ==\n") +path2Script <- file.path(moduledir, "Tools", "testMultiDataSet_wrapper.R") +status2 <- system2("Rscript", c(shQuote(path2Script)), stdout = TRUE, stderr = FALSE) +writeLines(status2) +check(any(grepl("MULTIDATASET-WRAPPER-OK", status2, fixed = TRUE)), "wrapper subprocess succeeded") + +# ------------------------------------------------------------------ Path 3: legacy still OK ---- +cat("\n== Path 3: legacy non-aware run (single dataset, plain names) ==\n") +# The aware analysis ALWAYS gets the bridge slice queue now (engine semantics: multiDataSetJson +# is produced whenever the Description says aware), so the legacy dataset/readDataSet path runs +# through a plain non-aware analysis instead of the removed twin: +# testEncodingFunc has no preloadData, so its R body really calls .readDataSetToEnd() and +# echoes the column values - a live witness that the readDataSet guard stays silent outside +# aware runs and that data survives the bridge encode->decode round trip. +res3 <- jaspTools::runAnalysis("testEncodingFunc", dataset = csvA, + options = list(selected = "Score"), + view = FALSE, quiet = TRUE) +raw3 <- jsonlite::toJSON(res3, auto_unbox = TRUE) +check(identical(res3$status, "complete"), "legacy run completed") +check(grepl("12.5", raw3, fixed = TRUE), + "legacy run read DataSetA's Score values through the (non-aware) readDataSet path") +check(grepl("encodingsEtc", raw3, fixed = TRUE) && grepl("\"Score\"", raw3, fixed = TRUE), + "the plain column name came back decoded (encode -> decode roundtrip on the legacy path too)") +check(!grepl("JASPColumn_", raw3), "legacy results decoded too") +check(!grepl("not available in multi-dataset", raw3, fixed = TRUE), + "guard demo: readDataSet() works normally outside aware runs") + +cat("\n") +if (failures > 0L) stop(failures, " multi-dataset test check(s) failed") +cat("All multi-dataset analysis tests passed\n") diff --git a/Tools/testMultiDataSet_wrapper.R b/Tools/testMultiDataSet_wrapper.R new file mode 100644 index 0000000..b9f2a14 --- /dev/null +++ b/Tools/testMultiDataSet_wrapper.R @@ -0,0 +1,91 @@ +#!/usr/bin/env Rscript +# Wrapper-path check of the multi-dataset analysis: no jaspTools, no JASP - just +# jaspTestModule::multiDataSetFunc(datasets = , ...) like an R user would call it, +# with the dataset-selection options naming which dataset each variables-form uses. +# Run by Tools/testMultiDataSet.R, but standalone-runnable too. +# +# Flow under test: loadDataSets -> workspace (titles, ids, encoders) -> QML binds the selection +# options first (depends), switching each form; column options then encode per dataset and the +# bridge queues one slice per distinct filter; runJaspResults hands the analysis encoded options + +# encoded slices keyed by FILTER id (the selection option value), titles in attr dataSetNames. + +moduledir <- normalizePath(file.path(dirname(sub("^--file=", "", grep("^--file=", commandArgs(FALSE), value = TRUE)[[1]])), "..")) +pkglib <- Sys.getenv("JASP_TEST_PKGLIB", file.path(moduledir, "ModuleBundleBuildDir", "build_dir", "jaspTestModule")) +toollib <- Sys.getenv("JASP_MODULE_TOOLLIB", normalizePath(file.path(moduledir, "..", ".toolLib"))) +stopifnot(dir.exists(pkglib), dir.exists(toollib)) +.libPaths(c(pkglib, toollib, .libPaths())) +Sys.setenv(QT_QPA_PLATFORM = "offscreen") + +suppressMessages(library(jaspTestModule)) + +csvA <- read.csv(file.path(moduledir, "Tools/testdata/DataSetA.csv")) # Score, Age, Group +csvB <- read.csv(file.path(moduledir, "Tools/testdata/DataSetB.csv")) # Score, Age, Condition + +res <- jaspTestModule::multiDataSetFunc( + datasets = list(haha = csvA, godverdomme = csvB), + dataSetA = "haha", + dataSetB = "godverdomme", + dependentA = "Score", # Score exists in both: form A picked haha + dependentB = c("Age", "Condition")) # Condition only in godverdomme +raw <- as.character(res$.__enclos_env__$private$getResults()) +j <- jsonlite::fromJSON(raw, simplifyVector = FALSE)$results + +columnValues <- function(tb, fieldName) { + fields <- vapply(tb$schema$fields, function(f) f$name, character(1)) + if (length(tb$data) > 0 && is.list(tb$data[[1]]) && any(names(tb$data[[1]]) %in% fields)) { + cells <- vapply(tb$data, function(row) paste(as.character(row[[fieldName]]), collapse = ", "), character(1)) + return(stats::setNames(cells, paste0("row", seq_along(cells)))) + } + idx <- which(fields == fieldName)[1] + d <- tb$data[[idx]] + rowKeys <- names(d)[vapply(d, length, integer(1)) > 0L] + stats::setNames(vapply(d[rowKeys], function(v) paste(as.character(v), collapse = ", "), character(1), USE.NAMES = FALSE), rowKeys) +} + +ok <- TRUE +check <- function(cond, label) { + cat(if (isTRUE(cond)) "PASS :" else "FAIL :", "wrapper -", label, "\n") + if (!isTRUE(cond)) ok <<- FALSE +} + +check(grepl("Aware run", raw, fixed = TRUE), "analysis ran in aware mode") + +ovTitle <- unname(columnValues(j$dataSetOverview, "title")) +ovId <- unname(columnValues(j$dataSetOverview, "id")) +ovNames <- unname(columnValues(j$dataSetOverview, "columnNames")) +check(setequal(ovTitle, c("haha", "godverdomme")), "both datasets handed over, user names as titles") +check(length(ovId) == 2L && all(grepl("^[0-9]+$", ovId)), "datasets keyed by (bridge) numeric dataset ids") +check(all(grepl("JASPColumn_", ovNames)), "slices arrive with encoded column names") + +# the bridge dataset ids behind each title, so routing can be asserted exactly +idByTitle <- stats::setNames(ovId, ovTitle) + +sv <- j$selectedVariables +svSrc <- columnValues(sv, "source") +svVar <- columnValues(sv, "variable") +svId <- columnValues(sv, "dataSetId") +svN <- columnValues(sv, "n") +svValues <- columnValues(sv, "values") + +check(all(grepl("^JASPColumn_[0-9]+_[0-9]+_Encoded$", svVar)), + "option values arrive in the encoded namespace (not the plain user names)") + +rowFor <- function(src, dsTitle) { + which(svSrc == src & svId == idByTitle[[dsTitle]]) +} + +iA <- rowFor("A", "haha"); iB1 <- rowFor("B", "godverdomme") +check(length(iA) == 1L && length(iB1) == 2L, "three rows routed: A->haha once, B->godverdomme twice") +check(all(svN == "10"), "every column computed over 10 rows (encoded names indexed directly)") +check(grepl(sprintf("mean %.2f", mean(csvA$Score)), svValues[[iA]], fixed = TRUE), + "form A Score = haha's Score (per-form dataset selection)") +# integer columns arrive from the syntax bridge as ordinal -> the demo shows first levels, not mean; +# the level list itself proves it is GODVERDOMME'S Age (31/45/28... would be haha's) +ageLevelsB <- paste(utils::head(unique(as.character(csvB$Age)), 5), collapse = ", ") +check(any(grepl(ageLevelsB, svValues[iB1], fixed = TRUE)), + "form B Age = godverdomme's Age (identically named column, different dataset)") +check(any(grepl("X, Y, Z", svValues[iB1], fixed = TRUE)), + "form B Condition found in godverdomme (only exists there)") + +cat(if (ok) "MULTIDATASET-WRAPPER-OK\n" else "MULTIDATASET-WRAPPER-FAILED\n") +quit(status = if (ok) 0L else 1L) diff --git a/Tools/testdata/DataSetA.csv b/Tools/testdata/DataSetA.csv new file mode 100644 index 0000000..0927de6 --- /dev/null +++ b/Tools/testdata/DataSetA.csv @@ -0,0 +1,11 @@ +Score,Age,Group +12.5,31,A +9.1,45,B +15.3,28,A +7.8,52,C +19.6,37,B +11.2,41,A +8.4,33,C +16.9,29,B +13.7,48,A +10.5,36,C diff --git a/Tools/testdata/DataSetB.csv b/Tools/testdata/DataSetB.csv new file mode 100644 index 0000000..6673ac3 --- /dev/null +++ b/Tools/testdata/DataSetB.csv @@ -0,0 +1,11 @@ +Score,Age,Condition +22.4,61,X +14.8,39,Y +18.2,44,X +9.9,50,Z +25.1,35,Y +12.6,58,X +21.3,42,Z +17.7,47,Y +11.8,53,X +23.5,38,Z diff --git a/Tools/verifyMultiDataSetRpc.py b/Tools/verifyMultiDataSetRpc.py new file mode 100644 index 0000000..e00d1fb --- /dev/null +++ b/Tools/verifyMultiDataSetRpc.py @@ -0,0 +1,1139 @@ +#!/usr/bin/env python3 +"""Verify jaspTestModule::multiDataSetFunc end-to-end through JASP's RPC server. + +Precondition: JASP (dev build) already running offscreen with RPC enabled, NO datasets. +Steps: load DataSetA + DataSetB (2 datasets in the workspace), create the multi-dataset +test analysis, and drive it the way the GUI dropdowns do: options dataSetA/dataSetB carry +FILTER IDS (form A -> filter 1 = dataset 1, form B -> filter 2 = dataset 2, simultaneously - +the linked-dropdown regression scenario), dependentA = Score (ds1) and dependentB = Score +(same name, ds2!) + Condition (only ds2). Never analysis_setFilter (that is the analysis- +global FilterMenuButton filter). Asserts: + - 'Datasets in this run' has 2 rows with titles + row/col counts + - 'Selected variables' rows have n=10 and the right means (Score/Score land on DIFFERENT + datasets: 12.50 vs 17.73 - no 'variable compared with itself') + - the metaDump echo audits the provenance contract as R RECEIVED it: + options$.meta carries no dataSetId/filterId and no .filterIds sidecar survives + the engine-boundary strip; per-form routing is proven at the VALUE level + (each form's variable resolves against its own selection's slice: 12.50 vs 17.73) + - no raw JASPColumn_* anywhere (decode on send) +And the live-verification block: switch one host's selection mid-session and +assert the next run only shows the LIVE selections - no phantom slice for the abandoned filter, +no provenance stamps on scalars or on the selection options themselves, column values restamped +onto the new filter (there and back). +Exit 0 = all good, 1 = failure (details printed). + +Run recipe (scripted crash-repro + verify loop): + 1. ~/.config/JASP/JASP.conf has rpcServerEnabled=true (and no JASP running). + 2. cd /build && QT_QPA_PLATFORM=offscreen ./Desktop/JASP + (under gdb to capture a backtrace if it dies: + gdb -batch -ex run -ex bt --args ./Desktop/JASP > /tmp/jasp_run.log 2>&1 &) + 3. python3 Tools/verifyMultiDataSetRpc.py +Before the jasp-desktop fix 785afe486 this died at analysis_create: the SEGV is +reproduced when JASP exits mid-script, backtrace in the gdb log. +""" + +# ---------------------------------------------------------------------------- +# Minimal JSON-RPC 2.0 client for JASP's RPC server (enabled by the preference +# rpcServerEnabled=true in ~/.config/JASP/JASP.conf; listens on port 48164, +# path /rpc by default). +# ---------------------------------------------------------------------------- +import json as _json, time as _time, urllib.request as _urllib + +URL = "http://127.0.0.1:48164/rpc" +_id = 0 + +def _rpc(method, params=None, timeout=60): + global _id + _id += 1 + body = _json.dumps({"jsonrpc": "2.0", "id": _id, "method": method, + "params": params or {}}).encode() + req = _urllib.Request(URL, data=body, headers={"Content-Type": "application/json"}) + with _urllib.urlopen(req, timeout=timeout) as r: + return _json.loads(r.read().decode()) + +def wait_up(max_wait=120): + start = _time.time() + while _time.time() - start < max_wait: + try: + r = _rpc("ping", timeout=2) + if "result" in r: + return r + except Exception: + _time.sleep(0.5) + raise RuntimeError("RPC server did not come up") + +call = _rpc + +import sys, json, time, os, re as _re + +# Locations: nothing machine-specific. The module dir is resolved as +# --module= (argv) > $JASP_TESTMODULE_DIR > the directory containing this script +# (it lives in /Tools/, so the default is self-locating). +def _findModuleDir(): + for a in sys.argv[1:]: + if a.startswith("--module="): + return os.path.abspath(a.split("=", 1)[1]) + env = os.environ.get("JASP_TESTMODULE_DIR") + if env: + return os.path.abspath(env) + return os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + +_moduleDir = _findModuleDir() +A_CSV = os.path.join(_moduleDir, "Tools", "testdata", "DataSetA.csv") +B_CSV = os.path.join(_moduleDir, "Tools", "testdata", "DataSetB.csv") + +fails = [] +def check(name, cond, extra=""): + print(("PASS " if cond else "FAIL ") + name + (f" {extra}" if extra else "")) + if not cond: + fails.append(name) + +def call(method, params=None, timeout=90): + r = _rpc(method, params, timeout=timeout) + if "error" in r: + print(f"RPC error on {method}: {json.dumps(r['error'])[:600]}") + return r.get("result", r) + +wait_up(120) +print("== RPC up ==") + +da = call("data_load", {"path": A_CSV, "wait": True, "timeoutMs": 60000}) +db = call("data_load", {"path": B_CSV, "wait": True, "timeoutMs": 60000}) +check("two datasets loaded (ids 1 and 2)", da.get("dataSetId") == 1 and db.get("dataSetId") == 2, + f"ids {da.get('dataSetId')}, {db.get('dataSetId')}") +i1 = call("data_info", {"dataSetId": 1}) +check("dataset 1 has Group (only in A)", any(c["name"] == "Group" for c in i1.get("columns", []))) + +an = call("analysis_create", {"module": "jaspTestModule", "analysis": "multiDataSetFunc"}) +check("form created without SEGV, analysis multiDataSetAware", an.get("status") == "success" and an.get("multiDataSetAware") is True) +aid = an.get("analysisId") + +# The FilterSelect dropdowns write their selection into options dataSetA/dataSetB (filter ids), +# exactly like the desktop GUI does - no analysis_setFilter anywhere (that is the analysis-GLOBAL +# filter, the FormMenuButton's; per-form selection must never touch it): +r1 = call("analysis_run", {"analysisId": aid, "wait": True, "timeoutMs": 60000, "options": { + "dataSetA": "1", + "dependentA": {"types": [], "value": ["Score"]}}}) +check("run 1 (dataSetA=f1, dependentA=Score) complete", r1.get("status") == "complete", str(r1.get("status"))) + +# The real regression scenario: form B selects filter 2 (dataset 2) WHILE form A stays on +# filter 1 - both selections live simultaneously, identically named column from each. +r2 = call("analysis_run", {"analysisId": aid, "wait": True, "timeoutMs": 120000, "options": { + "dataSetA": "1", + "dataSetB": "2", + "dependentA": {"types": [], "value": ["Score"]}, + "dependentB": {"types": [], "value": ["Score", "Condition"]}, + "showMeta": True}}) +check("second run complete", r2.get("status") == "complete", str(r2.get("status"))) + +# poll for completion +for _ in range(30): + st = call("get_analyses_state", {"analysisIds": [aid], "include_options": True, "include_results": False}) + a = (st.get("analyses") or [{}])[0] + if a.get("status") in ("complete", "empty", "noData", "running"): + if a.get("status") != "running": + break + time.sleep(1) +check("analysis status complete", a.get("status") == "complete", f"status={a.get('status')} errors={str(a.get('errors'))[:300]}") + +res = call("analysis_results", {"analysisId": aid, "wait": True, "timeoutMs": 60000}) +open("results_multidataset.json", "w").write(json.dumps(res, indent=1)) +import shutil, os +rds = res.get("jaspResultsRds") +if rds and os.path.exists(rds): + shutil.copy(rds, "jaspResults_copied.rds") +results = res.get("results", res) + +def tables(node, path=""): + out = {} + if isinstance(node, dict): + if "schema" in node and "data" in node: + out[path] = node + for k, v in node.items(): + out.update(tables(v, f"{path}/{k}")) + elif isinstance(node, list): + for i, v in enumerate(node): + out.update(tables(v, f"{path}[{i}]")) + return out + +tabs = tables(results) +print("tables found:", [(k, t.get("title")) for k, t in tabs.items()][:20]) + +def field_maps(t): + """jaspBase json, both layouts: column-major (data[i] is a {rowKey: value} map for schema + field i) and row-major (data[i] is one {field: value} dict per row, setData(data.frame)).""" + fields = [f["name"] for f in t["schema"]["fields"]] + data = t["data"] + if data and all(isinstance(e, dict) and set(e.keys()) <= set(fields) and e.keys() for e in data): + rows = {f"row{i}": r for i, r in enumerate(data)} + fm = {f: {k: r.get(f) for k, r in rows.items()} for f in fields} + return fields, fm, sorted(rows.keys()) + rowkeys = set() + for entry in data: + for key, val in entry.items(): + if val is not None and key not in fields: + rowkeys.add(key) + return fields, {f: data[i] for i, f in enumerate(fields)}, sorted(rowkeys) + +ds = next((t for t in tabs.values() if t.get("title") == "Datasets in this run"), None) +check("'Datasets in this run' table present", ds is not None) +if ds: + fields, fm, rks = field_maps(ds) + dump = json.dumps([fm[f].get(r) for r in rks for f in fields]) + check("2 dataset rows", len(rks) == 2, str(rks)) + titles = sorted(str(fm["title"].get(r)) for r in rks) + check("both dataset titles present (non-empty)", len(titles) == 2 and all(t not in ("None", "") for t in titles), str(titles)) + check("row counts 10/10", all(fm["rows"].get(r) == 10 for r in rks), str([fm["rows"].get(r) for r in rks])) + check("col counts present", all(isinstance(fm["cols"].get(r), int) for r in rks), str([fm["cols"].get(r) for r in rks])) + check("no raw JASPColumn_* in overview", "JASPColumn" not in dump, dump[:300]) + print("Datasets-in-this-run rows:", json.dumps([{f: fm[f].get(r) for f in fields} for r in rks])) + +sv = next((t for t in tabs.values() if t.get("title") == "Selected variables"), None) +check("'Selected variables' table present", sv is not None) +if sv: + fields, fm, rks = field_maps(sv) + dump = json.dumps({r: {f: fm[f].get(r) for f in fields} for r in rks}) + rows = [{f: fm[f].get(r) for f in fields} for r in rks] + def row(src, var): return next((x for x in rows if x["source"] == src and x["variable"] == var), None) + check("3 selected-variable rows (A:Score, B:Score, B:Condition)", len(rows) == 3, dump[:500]) + a_score = row("A", "Score") + b_score = row("B", "Score") + b_cond = row("B", "Condition") + check("A:Score routed to dataset 1", a_score and a_score["dataSetId"] in (1, "1") and a_score["dataSetName"] == "DataSet 1", json.dumps(a_score)) + check("B:Score (name in BOTH datasets) routed to dataset 2", b_score and b_score["dataSetId"] in (2, "2") and b_score["dataSetName"] == "DataSet 2", json.dumps(b_score)) + check("B:Condition (only in dataset 2) routed there", b_cond and b_cond["dataSetId"] in (2, "2"), json.dumps(b_cond)) + check("n=10 on every row", all(r["n"] == 10 for r in rows), str([r["n"] for r in rows])) + print("Selected variables rows:") + for r in rows: print(" ", json.dumps(r)) + check("scale rows show mean, factor row shows levels", + a_score and str(a_score["values"]).startswith("mean") and b_cond and str(b_cond["values"]) == "X, Y, Z", + f"{a_score['values'] if a_score else None!r} / {b_cond['values'] if b_cond else None!r}") + # the headline: same column name, two datasets, two different truths (NOT the same variable): + check("A:Score is DataSetA's Score (mean 12.50)", a_score and "12.50" in str(a_score["values"]), json.dumps(a_score)) + check("B:Score is DataSetB's Score (mean 17.73), not A's repeated", b_score and "17.73" in str(b_score["values"]), json.dumps(b_score)) + check("A:Score and B:Score are different rows with different data", bool(a_score and b_score and a_score["values"] != b_score["values"])) + check("decoded names, no raw JASPColumn_*", "JASPColumn" not in dump, dump[:300]) + +check("analysis references both datasets (dataSetIds in run response)", sorted(r2.get("dataSetIds", [])) == [1, 2], str(r2.get("dataSetIds"))) + +# Provenance as R sees it - NONE. The generated Engine json keeps +# .meta provenance-free and the transient .filterIds sidecar is stripped at the engine +# boundary; the metaDump html (options$showMeta was on) audits exactly that. Per-form +# provenance still routes the DATA (two same-named columns, two truths) - pinned value-level +# above: A:Score 12.50 vs B:Score 17.73. +meta_text = json.dumps(results).replace(chr(92)+chr(34), '"').replace(""", '"').replace(""", '"') +AUDIT_RE = r'filterIdsSeen=(\w+) metaProvenanceKeys=(\d+)' +def provenance_audit(res): + """(filterIdsSeen, metaProvenanceKeys) from the metaDump echo of a results payload.""" + text = json.dumps(res).replace(chr(92) + chr(34), '"').replace(""", '"').replace(""", '"') + m = _re.search(AUDIT_RE, text) + return (m.group(1), m.group(2)) if m else (None, None) +audit2 = provenance_audit(results) +check("metaDump ran (provenance audit present)", audit2[0] is not None, str(audit2)) +check("R never sees .filterIds (strip at the engine boundary)", audit2 == ("false", audit2[1]), str(audit2)) +check("options$.meta as R received it carries no provenance keys", audit2[1] == "0", str(audit2)) + +# ---------------------------------------------------------------------------- +# The phantom-slice class. Switch ONE HOST'S SELECTION +# mid-session and check the NEXT run's data shows only the LIVE selections: no slice queued +# for an abandoned filter, and the run's provenance still clean (the per-run audit). +# Since the sidecar flip the restamp moves the TYPED term filterIds (HostFilterSelection +# ::restampValuesUnderHost -> ListModel::restampTermsFilterId -> generated `.filterIds`), the +# .meta half is dead. This RPC workspace has one (default) filter per dataset, so the +# mid-session switch here crosses datasets (form A: f1 -> f2 -> f1); the sharp same-dataset/ +# other-filter case - where no encoded value changes and only the restamp walk can keep the +# sidecar honest - is pinned C++/QML-side by +# Tests/qmlTests/tst_dataSetSelectionVariablesList.qml::test_midsession_switch_restamps_column_values +# (and tst_generatedOptionsJsonAware.qml::test_aware_midswitch_sidecar_follows_without_stale_ids). +# ---------------------------------------------------------------------------- + +def run_and_results(opts, tag): + r = call("analysis_run", {"analysisId": aid, "wait": True, "timeoutMs": 120000, "options": opts}) + check(f"{tag} complete", r.get("status") == "complete", str(r.get("status"))) + res = call("analysis_results", {"analysisId": aid, "wait": True, "timeoutMs": 60000}) + return r, res + +def overview_ids(res): + t = next((x for x in tables(res.get("results", res)).values() if x.get("title") == "Datasets in this run"), None) + if not t: + return None, None + fields, fm, rks = field_maps(t) + return sorted(str(fm["id"].get(r)) for r in rks), t + +# switch form A to dataset 2's filter (both forms then sit on DataSet 2): +r3, res3 = run_and_results({ + "dataSetA": "2", + "dataSetB": "2", + "dependentA": {"types": [], "value": ["Score"]}, + "dependentB": {"types": [], "value": ["Score", "Condition"]}, + "showMeta": True}, "run 3 (form A switched to filter 2 mid-session)") + +ids3, _ = overview_ids(res3) +check("run 3: no phantom slice - only DataSet 2 loaded", ids3 == ["2"], str(ids3)) +check("run 3: dataSetIds not over-reported", sorted(r3.get("dataSetIds", [])) == [2], str(r3.get("dataSetIds"))) +audit3 = provenance_audit(res3) +check("run 3: still no provenance reaches R after the switch", audit3 == ("false", "0"), str(audit3)) +# (old stamp-window checks retired with the meta stamps, the provenance audit above +# covers scalars and selection options too - NO dataSetId/filterId key may appear ANYWHERE) + +sv3 = next((t for t in tables(res3.get("results", res3)).values() if t.get("title") == "Selected variables"), None) +rows3 = [] +if sv3: + f3, fm3, rk3 = field_maps(sv3) + rows3 = [{f: fm3[f].get(r) for f in f3} for r in rk3] +a3 = next((x for x in rows3 if x["source"] == "A" and x["variable"] == "Score"), None) +check("run 3: A:Score routed through the restamped key to DataSet 2 (mean 17.73)", + a3 and str(a3["dataSetId"]) == "2" and "17.73" in str(a3["values"]), json.dumps(a3)) + +# and back: form A returns to filter 1 - DataSet 1 must come back exactly once (no doubled slice): +r4, res4 = run_and_results({ + "dataSetA": "1", + "dataSetB": "2", + "dependentA": {"types": [], "value": ["Score"]}, + "dependentB": {"types": [], "value": ["Score", "Condition"]}, + "showMeta": True}, "run 4 (form A switched back to filter 1)") + +ids4, _ = overview_ids(res4) +check("run 4: exactly one slice per dataset again", ids4 == ["1", "2"], str(ids4)) +check("run 4: dataSetIds reports both datasets", sorted(r4.get("dataSetIds", [])) == [1, 2], str(r4.get("dataSetIds"))) +audit4 = provenance_audit(res4) +check("run 4: provenance contract held through the switch-back too", audit4 == ("false", "0"), str(audit4)) +a4 = None +sv4 = next((t for t in tables(res4.get("results", res4)).values() if t.get("title") == "Selected variables"), None) +if sv4: + f4, fm4, rk4 = field_maps(sv4) + a4 = next((x for x in ({f: fm4[f].get(r) for f in f4} for r in rk4) if x["source"] == "A" and x["variable"] == "Score"), None) +check("run 4: A:Score back on DataSet 1 (mean 12.50)", a4 and str(a4["dataSetId"]) == "1" and "12.50" in str(a4["values"]), json.dumps(a4)) + +# ---------------------------------------------------------------------------- +# the component zoo (jaspTestModule analysis multiDataSetControlsFunc / +# testMultiDataSetControls.qml). Every control family echoes what it RECEIVED (encoded names, +# decoded again on the way out), which slice index each value resolved in, and its row children, +# so per-item provenance is asserted at the value level. The RPC workspace starts with one +# default filter per dataset (ids 1 and 2) - exactly two distinct namespaces for the twin cases. +# The zoo form BOOTSTRAPS a third filter on dataset 1 ("Filter 0", all-rows) at form load, because +# the RPC has no filter-creation method and the zoo correction needs a THIRD pool +# hosted by it (the f3-origin namespace for the anchor scenario); its id is discovered below. +# Checks per family: +# - same-name columns across two filters in the SAME list (twins) -> distinct encodings, +# correct per-slice values (the 12.50/17.73 trick per family) +# - per-row provenance in the TableFields-style row control (row-bearing variables list): +# twin rows keep their own keep/shift children (the TermRowKey re-keying; pre-fix the +# second row clobbered the first) +# - FactorLevelList encodeThis roundtrip: factor/level names arrive usable and NOTHING in the +# final results leaks a raw JaspExtraOptions_ or JASPColumn_ token +# - interaction terms resolve as one unit in a single slice (uniform term-level filterId) +# - textarea host: tokens encode through the textarea's own selection and resolve there +# - cross-host bindTo: a typed copy carrying the SOURCE filterId resolves in the +# SOURCE's slice although the destination list selects the other one +# - scalars/combos stay plain; PROVENANCE_AUDIT clean on every zoo run +# ---------------------------------------------------------------------------- + +zoo = call("analysis_create", {"module": "jaspTestModule", "analysis": "multiDataSetControlsFunc"}) +check("zoo form created, analysis multiDataSetAware", zoo.get("status") == "success" and zoo.get("multiDataSetAware") is True) +zid = zoo.get("analysisId") + +def zoo_run(opts, tag): + r = call("analysis_run", {"analysisId": zid, "wait": True, "timeoutMs": 120000, "options": opts}) + check(f"zoo {tag} complete", r.get("status") == "complete", str(r.get("status"))) + res = call("analysis_results", {"analysisId": zid, "wait": True, "timeoutMs": 60000}) + return r, res + +def zoo_rows(res): + t = next((x for x in tables(res.get("results", res)).values() if x.get("title") == "Controls provenance"), None) + if not t: + return None + fields, fm, rks = field_maps(t) + return [{f: fm[f].get(r) for f in fields} for r in rks] + +def zoo_slice_ids(res): + t = next((x for x in tables(res.get("results", res)).values() if x.get("title") == "Datasets in this run"), None) + if not t: + return None + fields, fm, rks = field_maps(t) + return sorted(str(fm["id"].get(r)) for r in rks) + +def find(rows, family, pred=lambda r: True): + return [r for r in (rows or []) if r["family"] == family and pred(r)] + +# analysis_run options are a DELTA (form->parseOptions binds only the given members, everything +# else keeps its current bound value - verified live): every zoo run below sends the FULL explicit +# option set (families emptied, hosts deselected) so slice/dataSetIds expectations are exact. +EMPTY_TERMS = {"types": [], "value": []} +EMPTY_ROWS = {"types": [], "value": [], "optionKey": "variable"} +EMPTY_INTER = {"types": [], "value": [], "optionKey": "components"} +EMPTY_CODEF = {"modelOriginal": "", "model": "", "columns": []} + +def zoo_full_opts(host_a="1", host_m3="", host_d="", host_f="", host_g="", + host_j="", host_k="", + assigned_a=None, assigned_default=None, assigned_k=None, rows_c=None, bound_g=None, + assigned_three=None, assigned_derived=None, + interaction_d=None, factors_e=None, factors_k=None, pairs_j=None, code_f=None, + combo_a="", combo_i="", text_h="", rows_h=5, double_h=1.5, slider_h=0.5, + radio_h="mean", gate_h=False, gated_h=10.0, table_l=None, + plain_switch=False, show_meta=True): + # NB: dataSetB/dataSetC are GONE (zoo correction): Zoo B is REMOVED entirely (a + # bare assigned list - no source at all - is a malformed configuration, owner rule) and Zoo C + # is a normal form over a default-provider pool now - neither has a host option to send. + # dataSetM3 is Zoo M's extra-source pool (the THIRD filter of dataset 1, bootstrapped by the + # zoo form itself - see the discovery check right after zoo run 1). + opts = { + "dataSetA": host_a, "dataSetM3": host_m3, + "dataSetD": host_d, "dataSetF": host_f, "dataSetG": host_g, + "dataSetJ": host_j, "dataSetK": host_k, + "assignedA": assigned_a or dict(EMPTY_TERMS), + "assignedK": assigned_k or dict(EMPTY_TERMS), + "assignedDefault": assigned_default or dict(EMPTY_TERMS), + "assignedThree": assigned_three or dict(EMPTY_TERMS), + "assignedDerived": assigned_derived or dict(EMPTY_TERMS), + "rowsC": rows_c or dict(EMPTY_ROWS), + "boundG": bound_g or {"value": []}, + "interactionD": interaction_d or dict(EMPTY_INTER), + "factorsE": factors_e or [], + "factorsK": factors_k or [], + "pairsJ": pairs_j or dict(EMPTY_TERMS), + "codeF": code_f or dict(EMPTY_CODEF), + "comboA": combo_a, "comboI": combo_i, "comboG": "first", + "textH": text_h, "rowsH": rows_h, "doubleH": double_h, "sliderH": slider_h, + "radioH": radio_h, "gateH": gate_h, "gatedH": gated_h, + "plainSwitch": plain_switch, + } + if table_l is not None: + opts["tableL"] = table_l + if show_meta: + opts["showMeta"] = True + return opts + +# --- zoo run 1: sanity - plain list through host A, audit on ------------------------------------- +zr1, zres1 = zoo_run({ + "dataSetA": "1", + "assignedA": {"types": ["scale"], "value": ["Score"]}, + "showMeta": True}, "run 1 (plain list via host A)") +z1 = zoo_rows(zres1) +check("zoo run 1: provenance table present", z1 is not None) +a1 = find(z1, "assigned-list") +check("zoo run 1: assignedA Score resolves in slice 1 (12.50)", + len(a1) == 1 and str(a1[0]["slice"]) == "1" and "12.50" in str(a1[0]["values"]), json.dumps(a1)) +auditz1 = provenance_audit(zres1) +check("zoo run 1: PROVENANCE_AUDIT clean", auditz1 == ("false", "0"), str(auditz1)) +zslices1 = zoo_slice_ids(zres1) or [] +check("zoo run 1: slice 1 queued (host A), nothing beyond the workspace's two slices", + "1" in zslices1 and set(zslices1) <= {"1", "2"}, str(zslices1)) + +# --- the zoo fixture's THIRD filter (zoo correction) -------------------------------- +# The zoo form bootstraps "Filter 0" on dataset 1 at form load (zoo run 1 forced the form to +# load, so the bootstrap has run by now). Discover its id via data_info - the verifier must not +# hardcode 3 (a re-run in a workspace that already had extra filters would shift it). +_zi1 = call("data_info", {"dataSetId": 1}) +_zf3 = next((f for f in _zi1.get("filters", []) if not f.get("isDefault")), None) +check("zoo fixture: the form bootstrapped a third filter on dataset 1", _zf3 is not None, + json.dumps(_zi1.get("filters", []))) +F3 = str(_zf3["filterId"]) if _zf3 else "3" +check("zoo fixture: third filter id discovered", F3.isdigit() and F3 not in ("1", "2"), F3) + +# --- zoo run 2: the whole zoo --------------------------------------------------------------------- +# rowsC arrives in the STORAGE term-object shape: same-name twin rows 'Score' behind filter 1 +# (keep=true, shift=1) and filter 2 (keep=false, shift=7). The applier must keep BOTH rows' own +# children and each token must encode/resolve through ITS filter's view. +# boundG receives a typed copy of a list on filter 1 (Score@1) while its OWN selection is filter 2: +# the destination rule must not requalify the foreign stamp - it resolves in slice 1. +# codeF's host selects filter 1 while the workspace's SHOWN dataset is B (filter 2): the textarea's +# bind-time encodeRScript must go through the HOST's view (host-bound) - a Score token +# resolving in slice 1 (12.50) proves it; the analysis-level fallback would have yielded 17.73. +zr2, zres2 = zoo_run(zoo_full_opts( + host_a="1", host_d="1", host_f="1", host_g="2", + assigned_a={"types": ["scale"], "value": ["Score"]}, + assigned_default={"types": ["nominal"], "value": ["Condition"]}, + rows_c={"value": [{"variable": {"name": "Score", "types": ["scale"], "filterId": 1}, "keep": True, "shift": 1}, + {"variable": {"name": "Score", "types": ["scale"], "filterId": 2}, "keep": False, "shift": 7}], + "optionKey": "variable"}, + bound_g={"value": [{"name": "Score", "types": ["scale"], "filterId": 1}]}, + interaction_d={"types": [["scale", "scale"]], "value": [{"components": ["Score", "Age"]}], "optionKey": "components"}, + factors_e=[{"name": "Group", "levels": ["A", "B"]}], + code_f={"modelOriginal": "Score + Age", "model": "Score + Age", "columns": []}, + combo_a="Score", plain_switch=True), "run 2 (full zoo)") +z2 = zoo_rows(zres2) +dump2 = json.dumps(z2) if z2 is not None else "" +check("zoo run 2: provenance table present", z2 is not None) +print("zoo run 2 rows:") +for r in (z2 or []): + print(" ", json.dumps(r)) + +# components-rows: the flagship - twins with OWN children, distinct slices + values +rc = find(z2, "components-rows") +rc1 = next((r for r in rc if str(r["slice"]) == "1"), None) +rc2 = next((r for r in rc if str(r["slice"]) == "2"), None) +check("zoo run 2: rowsC twin rows kept DISTINCT encodings (two rows, slices 1 and 2)", + len(rc) == 2 and rc1 is not None and rc2 is not None, dump2[:400]) +check("zoo run 2: twin row @1 carries its OWN children keep=true shift=1 (+ 12.50)", + rc1 and "keep=TRUE" in rc1["rowChild"] and "shift=1," in rc1["rowChild"] + "," and "12.50" in rc1["values"], json.dumps(rc1)) +check("zoo run 2: twin row @2 carries its OWN children keep=false shift=7 (+ 17.73)", + rc2 and "keep=FALSE" in rc2["rowChild"] and "shift=7" in rc2["rowChild"] and "17.73" in rc2["values"], json.dumps(rc2)) +check("zoo run 2: decoded names, no raw JASPColumn_* in any row", "JASPColumn" not in dump2, dump2[:300]) + +# cross-host bindTo +bg = find(z2, "bindto-cross-host") +check("zoo run 2: boundG typed copy resolves in the SOURCE slice 1 (12.50) while its own host selects filter 2", + len(bg) == 1 and str(bg[0]["slice"]) == "1" and "12.50" in str(bg[0]["values"]), json.dumps(bg)) + +# default-provider pool's list (Zoo M): a bare sent term resolves through the analysis-level +# fallback, which is the shown dataset (DataSet 2, filter 2) +ab = find(z2, "pool-hostless") +check("zoo run 2: assignedDefault (bare) Condition resolves via the analysis fallback in slice 2 with its levels", + len(ab) == 1 and str(ab[0]["slice"]) == "2" and ab[0]["values"].startswith("X"), json.dumps(ab)) + +# combobox (Zoo A): plain string value. NB the namespace finding, UPDATED by the zoo +# expansion: comboA's `source` used to be a broken QML binding (`source: zooVarsA` - a bare +# identifier that does not exist; ReferenceError in the runtime log), so the dropdown silently +# had no option list and its value encoded through the analysis-level namespace. With the source +# fixed to the string-name form ("zooVarsA") a dropdown INHERITS ITS SOURCE's host namespace: +# the value now encodes through zooVarsA's host (dataSetA). Run 9 pins that strictly; this +# legacy check keeps its tolerance (the value stays plain and roundtrips either way). +ca = find(z2, "combobox-host") +check("zoo run 2: comboA 'Score' stays plain and resolves in exactly one slice (analysis-level namespace)", + len(ca) == 1 and ca[0]["received"] == "Score" and str(ca[0]["slice"]) in ("1", "2"), json.dumps(ca)) + +# interaction (Zoo D): one term, components share the slice +itr = find(z2, "interaction-list") +check("zoo run 2: interaction Score:Age (host D=1) resolves as one unit in slice 1", + len(itr) == 1 and str(itr[0]["slice"]) == "1" and "share slice 1" in str(itr[0]["values"]), json.dumps(itr)) + +# factor levels (Zoo E): extras roundtrip - names usable, decoded, no leakage +fe = find(z2, "factor-levels") +check("zoo run 2: factorsE echoes Group with levels A, B (decoded and usable)", + len(fe) == 1 and fe[0]["received"] == "Group" and "A, B" in fe[0]["rowChild"], json.dumps(fe)) + +# textarea host (Zoo F): tokens resolved through dataSetF's slice (1) - the SHARP host-bound proof: +# the workspace's shown dataset is B (filter 2), so 17.73 here would mean the analysis-level +# fallback won and the host declaration was ignored. +tf = find(z2, "textarea-host") +tf_scores = [r for r in tf if r["received"] == "Score"] +tf_ages = [r for r in tf if r["received"] == "Age"] +tf_texts = [r for r in tf if r["option"] == "codeF(text)"] +check("zoo run 2: codeF host-1 tokens: Score resolves in slice 1 (12.50, NOT the shown-dataset 17.73)", + len(tf_scores) >= 1 and all(str(r["slice"]) == "1" for r in tf_scores) and any("12.50" in r["values"] for r in tf_scores), json.dumps(tf_scores)) +check("zoo run 2: codeF host-1 tokens: Age resolves in slice 1", + len(tf_ages) >= 1 and all(str(r["slice"]) == "1" for r in tf_ages) and any(r["values"] not in ("", "None", None) for r in tf_ages), json.dumps(tf_ages)) +check("zoo run 2: codeF model text round-trips (decoded on the way out)", + len(tf_texts) == 1 and tf_texts[0]["received"] == "Score + Age", json.dumps(tf_texts)) + +# scalars stay plain: echo rows exist with the plain values +sc = find(z2, "scalar") +cg = find(z2, "combobox-plain") +check("zoo run 2: scalar plainSwitch echoes plain true (never provenanced)", len(sc) == 1 and sc[0]["received"].upper() == "TRUE", json.dumps(sc)) +check("zoo run 2: plain combo comboG echoes 'first' (never term-objectified)", len(cg) == 1 and cg[0]["received"] == "first", json.dumps(cg)) + +# slices: hosts on filters 1+2 (A,D on 1; B,F,G on 2) + every referenced value's slice +zslices2 = zoo_slice_ids(zres2) +check("zoo run 2: both slices present, no phantom", zslices2 == ["1", "2"], str(zslices2)) +auditz2 = provenance_audit(zres2) +check("zoo run 2: PROVENANCE_AUDIT clean", auditz2 == ("false", "0"), str(auditz2)) +fullres2 = json.dumps(zres2) +check("zoo run 2: NO raw JASPColumn_* anywhere in results", "JASPColumn" not in fullres2) +check("zoo run 2: NO raw JaspExtraOptions_ leakage anywhere in results", "JaspExtraOptions" not in fullres2) + +# --- zoo run 3: same-name twins in the PLAIN assigned list (storage term objects) ----------------- +zr3, zres3 = zoo_run(zoo_full_opts( + host_a="1", + assigned_a={"value": [{"name": "Score", "types": ["scale"], "filterId": 1}, + {"name": "Score", "types": ["scale"], "filterId": 2}]}, + show_meta=False), "run 3 (plain-list twins, everything else reset)") +z3 = zoo_rows(zres3) +al = find(z3, "assigned-list") +t1 = next((r for r in al if str(r["slice"]) == "1"), None) +t2 = next((r for r in al if str(r["slice"]) == "2"), None) +check("zoo run 3: assignedA twin rows distinct (both 'Score', two slices, two truths)", + len(al) == 2 and t1 and t2 and "12.50" in t1["values"] and "17.73" in t2["values"], json.dumps(al)) +check("zoo run 3: dataSetIds covers both datasets", sorted(zr3.get("dataSetIds", [])) == [1, 2], str(zr3.get("dataSetIds"))) + +# --- zoo run 4: reset to a single plain family on host A (baseline for the mid-session switch) ---- +zr4, zres4 = zoo_run(zoo_full_opts(host_a="1", + assigned_a={"types": ["scale"], "value": ["Score"]}), + "run 4 (reset: plain list only, host A=1)") +z4 = zoo_rows(zres4) +a4z = find(z4, "assigned-list") +check("zoo run 4: assignedA Score on slice 1 (12.50)", + len(a4z) == 1 and "12.50" in a4z[0]["values"] and str(a4z[0]["slice"]) == "1", json.dumps(a4z)) + +# --- zoo run 5: mid-session host switch (delta run: only the host option changes) ----------------- +# NB: the value IS re-sent (same terms as run 4) - analysis_run options are a delta, but the +# point here is the RE-ENCODE through the NEW host namespace, which only happens when the option +# is part of the run (an untouched option keeps its already-encoded value). The zoo's primary +# slice (the analysis' own dataset) is queued besides the sidecar, so only "no dataset beyond +# the workspace's two" can be asserted here - the sharp claim is the row: Score must now resolve +# in slice 2 (17.73), i.e. the restamp walk + sidecar followed host A to DataSet 2. +zr5, zres5 = zoo_run(zoo_full_opts(host_a="2", + assigned_a={"types": ["scale"], "value": ["Score"]}), + "run 5 (mid-session switch host A -> filter 2)") +z5 = zoo_rows(zres5) +a5z = find(z5, "assigned-list") +check("zoo run 5: mid-session switch re-encoded the value to slice 2 (17.73)", + len(a5z) == 1 and str(a5z[0]["slice"]) == "2" and "17.73" in a5z[0]["values"], json.dumps(a5z)) +check("zoo run 5: slice 2 queued, nothing beyond the workspace's two datasets", + "2" in (zoo_slice_ids(zres5) or []) and set(zoo_slice_ids(zres5) or []) <= {"1", "2"}, str(zoo_slice_ids(zres5))) +check("zoo run 5: dataSetIds covers DataSet 2 and nothing else", + 2 in zr5.get("dataSetIds", []) and set(zr5.get("dataSetIds", [])) <= {1, 2}, str(zr5.get("dataSetIds"))) +auditz5 = provenance_audit(zres5) +check("zoo run 5: PROVENANCE_AUDIT clean after the switch", auditz5 == ("false", "0"), str(auditz5)) + +# --- zoo run 6: and back --------------------------------------------------------------------------- +zr6, zres6 = zoo_run(zoo_full_opts(host_a="1", + assigned_a={"types": ["scale"], "value": ["Score"]}), + "run 6 (switch back host A -> filter 1)") +z6 = zoo_rows(zres6) +a6z = find(z6, "assigned-list") +check("zoo run 6: switched back, Score resolves in slice 1 again (12.50)", + len(a6z) == 1 and str(a6z[0]["slice"]) == "1" and "12.50" in a6z[0]["values"], json.dumps(a6z)) + +# --- zoo runs 7/8: the cross-host typed copy against live host switches --------------------- +# run 7: source host A on filter 2, destination host G on filter 1; assignedA binds unqualified +# (intake stamps it @2) and boundG receives the typed copy Score@2. Both values must resolve in +# slice 2 - the copy through its SOURCE stamp, not through its own host's filter 1. +zr7, zres7 = zoo_run(zoo_full_opts( + host_a="2", host_g="1", + assigned_a={"types": ["scale"], "value": ["Score"]}, + bound_g={"value": [{"name": "Score", "types": ["scale"], "filterId": 2}]}), "run 7 (copy @2, dest host on 1)") +z7 = zoo_rows(zres7) +a7z = find(z7, "assigned-list") +bg7 = find(z7, "bindto-cross-host") +check("zoo run 7: assignedA (own host A=2) resolves in slice 2 (17.73)", + len(a7z) == 1 and str(a7z[0]["slice"]) == "2" and "17.73" in a7z[0]["values"], json.dumps(a7z)) +check("zoo run 7: boundG typed copy keeps the SOURCE stamp: slice 2 (17.73) although its own host G=1", + len(bg7) == 1 and str(bg7[0]["slice"]) == "2" and "17.73" in bg7[0]["values"], json.dumps(bg7)) +check("zoo run 7: dataSetIds covers source (ds2) and destination host (ds1)", sorted(zr7.get("dataSetIds", [])) == [1, 2], str(zr7.get("dataSetIds"))) +auditz7 = provenance_audit(zres7) +check("zoo run 7: PROVENANCE_AUDIT clean", auditz7 == ("false", "0"), str(auditz7)) + +# run 8: switch the SOURCE host A back to filter 1 (delta run: boundG is NOT re-sent, its typed +# term state carries the foreign stamp 2). The owns-the-stamp rule must restamp ONLY the +# host-owned term: assignedA follows A to slice 1, boundG stays on the SOURCE stamp @2 - the copy +# is a stable stamp, not a live reference; requalification to the (now matching) filter 1 would +# show up as 12.50 and fail here. +zr8, zres8 = zoo_run({"dataSetA": "1", "showMeta": True}, "run 8 (source host switched, copy not re-sent)") +z8 = zoo_rows(zres8) +a8z = find(z8, "assigned-list") +bg8 = find(z8, "bindto-cross-host") +check("zoo run 8: assignedA followed its OWN host to slice 1 (12.50)", + len(a8z) == 1 and str(a8z[0]["slice"]) == "1" and "12.50" in a8z[0]["values"], json.dumps(a8z)) +check("zoo run 8: boundG foreign stamp NOT requalified by the source-host switch (still slice 2, 17.73)", + len(bg8) == 1 and str(bg8[0]["slice"]) == "2" and "17.73" in bg8[0]["values"], json.dumps(bg8)) +auditz8 = provenance_audit(zres8) +check("zoo run 8: PROVENANCE_AUDIT clean", auditz8 == ("false", "0"), str(auditz8)) + +# ---------------------------------------------------------------------------- +# Zoo expansion - the NEW control families (Zoo H-L), the RESULTS side and +# the option flows. The point of the expansion: data and flows must be VISIBLE +# and the verifier must assert RESULTS-SIDE content, not just option roundtrips. +# +# Results inventory asserted below: +# "Loaded data - (slice <key>...)" the actual loaded DATA per slice: +# every column's decoded name AND its first `rowsH` values (head per slice) +# "Controls provenance" extended with the new families +# "Scalar & simple control echoes" scalar families as received + used-in +# "Option flows" the flow demonstrations +# "Cross-slice computations" computations across slices/lists of different provenance +# "encodeThis recode demo" Zoo E's levels APPLIED to a real column +# "Gated sub-result" EXISTS ONLY while gateH is on (a gate means absence) +# +# Flows (trigger -> effect, each verified by a delta run + results-side diff): +# F1 assignedA content -> comboI's option list & effective selection (dependent dropdown) +# F2 gateH checkbox -> the gated sub-result exists / vanishes; gatedH shifts its numbers +# F3 rowsH integer -> preview row count AND Zoo L's table row count +# F4 interactionD terms -> the interaction-product rows of the cross-slice table +# F5 tableL cells -> the multiplier statistic (cells are USED, not just echoed) +# F6 radioH -> the radio-driven statistic (sum vs mean) +# +# Expected statistics are computed from the same CSVs the datasets load, so the +# assertions are value-level. Slice column composition follows from the stamped +# namespaces (each asserted row documents where its term was stamped). +# ---------------------------------------------------------------------------- + +import csv as _csvmod + +def _read_columns(path): + with open(path) as f: + rows = list(_csvmod.DictReader(f)) + cols = {} + for k in rows[0].keys(): + vals = [r[k] for r in rows] + try: + cols[k] = [float(v) for v in vals] + except ValueError: + cols[k] = vals + return cols + +A = _read_columns(A_CSV) +B = _read_columns(B_CSV) + +# Age imports ORDINAL in DataSetA (integer-valued column; data_info: Age=ordinal, Score=scale, +# Group=nominal). A term sent as scale is re-typed to the column's real type at intake (the +# established zoo finding, pinned in Zoo K), so any Age slice arrives as an R factor and the +# echo describes it as its unique-value listing - never as a mean. The anchor checks (runs +# 19/20) therefore assert the listing, computed exactly like R's .fillDescription does: +with open(A_CSV) as _f: + _age_raw = [r["Age"] for r in _csvmod.DictReader(_f)] +_age_listing = ", ".join(list(dict.fromkeys(_age_raw))[:5]) + +def fmt(x, nd=2): return f"{x:.{nd}f}" +def mean(v): return sum(v) / len(v) + +def find_table(res, title_prefix): + return next((t for t in tables(res.get("results", res)).values() + if str(t.get("title", "")).startswith(title_prefix)), None) + +import html as _htmlmod + +def table_rows(t): + if not t: return [] + fields, fm, rks = field_maps(t) + # jaspBase HTML-escapes string cells ("->" arrives as "->"); unescape for assertions + return [{f: _htmlmod.unescape(str(fm[f].get(r))) if isinstance(fm[f].get(r), str) else fm[f].get(r) + for f in fields} for r in rks] + +def no_raw_tokens(res, tag): + raw = json.dumps(res) + check(f"{tag}: no raw JASPColumn_* in results", "JASPColumn" not in raw) + check(f"{tag}: no raw JaspExtraOptions_* in results", "JaspExtraOptions" not in raw) + +# --- zoo run 9: the expanded zoo, all new families populated -------------------------------------- +# Provenance story (fresh RPC session, shown dataset = DataSet 2): +# assignedA Score via host A(1) -> slice 1 | assignedDefault bare Score -> analysis fallback +# (shown dataset = DataSet 2) -> slice 2 +# assignedK Group+Age via host K(1) -> slice 1 (Group feeds the encodeThis demo, Age is the +# factor term's source) | pairsJ Score+Age via host J(1) -> slice 1 +# interactionD Score:Age via host D(1) -> slice 1 | factorsK Age -> the per-factor sidecar +# (FactorsFormBase::createFilterIds) transports the term's OWN filterId per +# factor and per term -> slice 1 (the term's stamp comes from its source host assignedK; the +# the hostless-panel deferral - analysis-level namespace, slice 2 - is CLOSED, pinned here) +zr9, zres9 = zoo_run(zoo_full_opts( + host_a="1", host_d="1", host_f="1", host_g="2", host_j="1", host_k="1", + assigned_a={"types": ["scale"], "value": ["Score"]}, + assigned_default={"types": ["scale"], "value": ["Score"]}, + # NB: the FactorsForm's factors can only hold terms that are in its SOURCE list (assignedK): + # a term outside the source is silently dropped by the form's model sync (verified live, run + # 9 originally sent Age here while assignedK held only Group -> "(none)"). Group is needed in + # the slice for the encodeThis demo, Age for the factor term, so the source carries both. + assigned_k={"types": ["nominal", "scale"], "value": ["Group", "Age"]}, + interaction_d={"types": [["scale", "scale"]], "value": [{"components": ["Score", "Age"]}], "optionKey": "components"}, + factors_e=[{"name": "Group", "levels": ["A", "B"]}], + factors_k=[{"name": "Fac1", "title": "Factor 1", "indicators": {"value": ["Age"], "types": [["scale"]]}}], + pairs_j={"types": ["scale", "scale"], "value": ["Score", "Age"]}, + combo_a="Score", combo_i="Score", text_h="hello zoo", rows_h=4, double_h=2.0, slider_h=0.3, + radio_h="sum", gate_h=True, gated_h=12.0), "run 9 (expanded zoo: all new families)") + +z9 = zoo_rows(zres9) +check("zoo run 9: provenance table present", z9 is not None) + +# (a) THE LOADED DATA - per-slice previews with real column names and values +pv1 = find_table(zres9, "Loaded data - DataSet 1") +pv2 = find_table(zres9, "Loaded data - DataSet 2") +check("zoo run 9: loaded-data preview for slice 1 present", pv1 is not None, + str([t.get('title') for t in tables(zres9.get('results', zres9)).values()])) +check("zoo run 9: loaded-data preview for slice 2 present", pv2 is not None) +if pv1 and pv2: + check("zoo run 9: preview titles say 'first 4 rows' (rowsH flow)", + "first 4 rows" in pv1["title"] and "first 4 rows" in pv2["title"], + f"{pv1['title']!r} / {pv2['title']!r}") + r1, r2 = table_rows(pv1), table_rows(pv2) + check("zoo run 9: preview row count == rowsH (4)", len(r1) == 4 and len(r2) == 4, + f"{len(r1)}/{len(r2)}") + # values: slice 1 is DataSetA, slice 2 is DataSetB - the whole point + check("zoo run 9: slice 1 shows DataSetA's Score values (12.5, 9.1, ...)", + any(abs((row.get("Score") or -999) - A["Score"][i]) < 1e-9 for i, row in enumerate(r1)), + json.dumps(r1)[:200]) + check("zoo run 9: slice 2 shows DataSetB's Score values (22.4, 14.8, ...) - NOT a repeat of A", + any(abs((row.get("Score") or -999) - B["Score"][i]) < 1e-9 for i, row in enumerate(r2)), + json.dumps(r2)[:200]) + check("zoo run 9: slice 1 shows the Group levels (nominal values, not numbers)", + any(row.get("Group") in ("A", "B", "C") for row in r1), json.dumps(r1)[:200]) + check("zoo run 9: previews carry decoded names, no raw tokens (fields)", + all("JASPColumn" not in f for f in [f["name"] for f in pv1["schema"]["fields"]] + + [f["name"] for f in pv2["schema"]["fields"]])) + +# (b) per-family provenance echoes for the new families +dep = find(z9, "dropdown-dependent") +check("zoo run 9: dependent dropdown comboI='Score' resolves in slice 1 via its SOURCE host (12.50)", + len(dep) == 1 and dep[0]["received"] == "Score" and str(dep[0]["slice"]) == "1" and "12.50" in dep[0]["values"], + json.dumps(dep)) +ca9 = find(z9, "combobox-host") +check("zoo run 9: comboA (fixed source binding) encodes through its SOURCE host namespace -> slice 1", + len(ca9) == 1 and str(ca9[0]["slice"]) == "1" and "12.50" in ca9[0]["values"], json.dumps(ca9)) + +pj = find(z9, "pairs-list") +check("zoo run 9: pairsJ one PAIR 'Score + Age', both components in slice 1 (host J)", + len(pj) == 1 and pj[0]["received"] == "Score + Age" and str(pj[0]["slice"]) == "1", json.dumps(pj)) +import statistics as _stats +_r = _stats.correlation(A["Score"], A["Age"]) +check("zoo run 9: pairsJ echo computes the pair correlation r=%.3f" % _r, + len(pj) == 1 and f"r = {fmt(_r, 3)}" in str(pj[0]["values"]), json.dumps(pj)) + +fk = find(z9, "factors-form") +check("zoo run 9: factorsK Fac1 received with title and var Age", + len(fk) == 1 and fk[0]["received"] == "Fac1" and "title=Factor 1" in fk[0]["rowChild"] + and "Age" in fk[0]["rowChild"], json.dumps(fk)) +check("zoo run 9: factorsK var Age resolves in ITS OWN term filter's namespace (slice 1) - the per-factor sidecar closed the hostless-panel deferral", + len(fk) == 1 and str(fk[0]["slice"]) == "1" and "slices: 1" in str(fk[0]["values"]), json.dumps(fk)) +check("zoo run 9: factorsK resolution is TERM-level inside the factor object (rowChild carries the per-term slice key 'Age(1)')", + len(fk) == 1 and "vars: Age(1)" in str(fk[0]["rowChild"]), json.dumps(fk)) + +fs = [r for r in z9 if r["family"] == "factors-source" and r["received"] == "Group"] +check("zoo run 9: assignedK Group (the factor SOURCE) resolves in ITS OWN host's slice 1 with levels", + len(fs) == 1 and str(fs[0]["slice"]) == "1" and fs[0]["values"].startswith("A, B, C"), json.dumps(fs)) +fs_age = [r for r in z9 if r["family"] == "factors-source" and r["received"] == "Age"] +check("zoo run 9: assignedK Age (second source var) resolves in slice 1 with its values (31, 45, ...)", + len(fs_age) == 1 and str(fs_age[0]["slice"]) == "1" and str(fs_age[0]["values"]).startswith("31, 45, 28"), + json.dumps(fs_age)) + +tv = find(z9, "tableview") +check("zoo run 9: tableL echo '4 rows x 2 cols' (row count follows rowsH=4 via the QML binding)", + len(tv) == 1 and "4 rows x 2 cols" in tv[0]["received"], json.dumps(tv)) + +# (c) scalar echoes +se = table_rows(find_table(zres9, "Scalar & simple control echoes")) +se_map = {r["option"]: r for r in se} +check("zoo run 9: scalar echo table has all 7 scalar families", + set(se_map) == {"textH", "rowsH", "doubleH", "sliderH", "radioH", "gateH", "gatedH"}, str(sorted(se_map))) +check("zoo run 9: textH echoes verbatim", se_map.get("textH", {}).get("received") == "hello zoo", json.dumps(se)) +check("zoo run 9: rowsH/doubleH/sliderH/radioH echo as received", + se_map.get("rowsH", {}).get("received") == "4" and abs(float(se_map.get("doubleH", {}).get("received") or 0) - 2.0) < 1e-9 + and se_map.get("sliderH", {}).get("received") == "0.3" and se_map.get("radioH", {}).get("received") == "sum", + json.dumps([se_map.get(k) for k in ("rowsH", "doubleH", "sliderH", "radioH")])) +check("zoo run 9: gateH true and gatedH annotated 'gate OPEN'", + se_map.get("gateH", {}).get("received") == "TRUE" and "gate OPEN" in str(se_map.get("gatedH", {}).get("received")), + json.dumps([se_map.get("gateH"), se_map.get("gatedH")])) + +# (d) flows table +fl = table_rows(find_table(zres9, "Option flows")) +fl_map = {r["flow"]: r for r in fl} +check("zoo run 9: flows table has the 5 flow rows", + set(fl_map) == {"dependent dropdown", "checkbox gate", "number field -> shown rows", + "radio -> statistic", "variables list -> interaction terms"}, str(sorted(fl_map))) +check("zoo run 9: dependent-dropdown flow observed 'Score -> slice 1'", + '"Score" -> slice 1' in str(fl_map.get("dependent dropdown", {}).get("observed")), json.dumps(fl)) +check("zoo run 9: gate flow observed OPEN", "gate OPEN" in str(fl_map.get("checkbox gate", {}).get("observed")), json.dumps(fl)) +check("zoo run 9: rows flow observed rowsH=4, previews 4 rows, tableL 4 rows", + "rowsH=4" in str(fl_map.get("number field -> shown rows", {}).get("observed")) + and "4 rows" in str(fl_map.get("number field -> shown rows", {}).get("observed")), json.dumps(fl)) + +# (e) cross-slice computations - value-level, computed from the CSVs +cs = table_rows(find_table(zres9, "Cross-slice computations")) +cs_map = {r["computation"]: r for r in cs} +_exp_prod = mean([a * b for a, b in zip(A["Score"], A["Age"])]) +check("zoo run 9: interaction product mean(Score*Age) in slice 1 = %s" % fmt(_exp_prod, 3), + f"mean of product {fmt(_exp_prod, 3)}" in str(cs_map.get("interaction product", {}).get("value")), json.dumps(cs)) +_exp_cross = mean([a * b for a, b in zip(A["Score"], B["Score"])]) +check("zoo run 9: cross-host product Score@1 x Score@2 = %s (two different truths, one number)" % fmt(_exp_cross, 3), + f"mean of product {fmt(_exp_cross, 3)}" in str(cs_map.get("cross-host column product", {}).get("value")) + and "DataSet 1" in str(cs_map.get("cross-host column product", {}).get("inputs")) + and "DataSet 2" in str(cs_map.get("cross-host column product", {}).get("inputs")), json.dumps(cs)) +# slice 1's first numeric column is Age (wanted-cols are set-sorted): Age mean 38.00 +check("zoo run 9: tableview multipliers mean([1,2,3,4])=2.5 x mean(Age@1)=38.00 -> 95.000", + "scaled mean 95.000" in str(cs_map.get("tableview multipliers", {}).get("value")), json.dumps(cs)) +check("zoo run 9: doubleH=2.0 scaling -> 76.000", "scaled mean 76.000" in str(cs_map.get("real-field scaling", {}).get("value")), json.dumps(cs)) +check("zoo run 9: radio-driven statistic sum(Score@1) = 125.00", + "sum 125.00" in str(cs_map.get("radio-driven statistic", {}).get("value")), json.dumps(cs)) + +# (f) encodeThis recode demo: factorsE levels APPLIED to the Group column of slice 1 +ed = table_rows(find_table(zres9, "encodeThis recode demo")) +ed_map = {r["oldValue"]: r for r in ed} +check("zoo run 9: encodeDemo decodes the extras factor name to 'Group' and finds it in slice 1", + all(r["slice"] == "1" for r in ed) and len(ed) == 3, json.dumps(ed)) +check("zoo run 9: encodeDemo recode A->A (n=4), B->B (n=3), C->B (n=3) - levels applied in order, capped", + ed_map.get("A", {}).get("newLabel") == "A" and ed_map.get("A", {}).get("n") == 4 + and ed_map.get("B", {}).get("newLabel") == "B" and ed_map.get("B", {}).get("n") == 3 + and ed_map.get("C", {}).get("newLabel") == "B" and ed_map.get("C", {}).get("n") == 3, json.dumps(ed)) + +# (g) gated sub-result EXISTS while the gate is open; first numeric column of each slice is Age +g9 = table_rows(find_table(zres9, "Gated sub-result")) +def _gated_map(g): + # slice cell is "1 (DataSet 1)"; meanAbove arrives as a number + return {(str(r["slice"]).split(" ")[0], r["column"]): r for r in g} + +# (g) gated sub-result EXISTS while the gate is open. "First scale column" per slice: +# slice 1's is Age (scale); slice 2's factor-term Age arrives ORDINAL (the FactorsForm +# re-types its terms - documented finding) hence as an R factor, so firstScaleColumn +# picks Score there. Both deterministic. +g9_map = _gated_map(g9) +check("zoo run 9: gated sub-result present with threshold 12 in the title", + find_table(zres9, "Gated sub-result") is not None + and "12" in find_table(zres9, "Gated sub-result")["title"], json.dumps(g9)) +check("zoo run 9: gated Age@1: all 10 rows above 12 (mean 38.00)", + g9_map.get(("1", "Age"), {}).get("nAbove") == 10 + and abs((g9_map.get(("1", "Age"), {}).get("meanAbove") or -999) - mean(A["Age"])) < 5e-3, json.dumps(g9)) +check("zoo run 9: gated Score@2 (factor-term Age arrives ordinal/factor, skipped): n=8 mean 19.45", + g9_map.get(("2", "Score"), {}).get("nAbove") == 8 + and abs((g9_map.get(("2", "Score"), {}).get("meanAbove") or -999) - mean([s for s in B["Score"] if s > 12])) < 5e-3, + json.dumps(g9)) + +check("zoo run 9: dataSetIds covers both datasets", sorted(zr9.get("dataSetIds", [])) == [1, 2], str(zr9.get("dataSetIds"))) +auditz9 = provenance_audit(zres9) +check("zoo run 9: PROVENANCE_AUDIT clean", auditz9 == ("false", "0"), str(auditz9)) +no_raw_tokens(zres9, "zoo run 9") + +# --- zoo run 10 (FLOW F1): the dependent dropdown follows its source ------------------------------- +# assignedA flips to Condition (host A=2): comboI's option list must follow, its effective +# selection re-encodes through the source's NEW namespace (slice 2), and the RESULTS change. +zr10, zres10 = zoo_run(zoo_full_opts( + host_a="2", host_d="1", host_f="1", host_g="2", host_j="1", host_k="1", + assigned_a={"types": ["nominal"], "value": ["Condition"]}, + assigned_k={"types": ["nominal"], "value": ["Group"]}, + factors_e=[{"name": "Group", "levels": ["A", "B"]}], + combo_i="Condition", text_h="hello zoo", rows_h=4, double_h=2.0, slider_h=0.3, + radio_h="sum", gate_h=True, gated_h=12.0), "run 10 (F1: dependent dropdown follows reassigned source)") +z10 = zoo_rows(zres10) +dep10 = find(z10, "dropdown-dependent") +check("zoo run 10: comboI='Condition' received (option list followed assignedA)", + len(dep10) == 1 and dep10[0]["received"] == "Condition", json.dumps(dep10)) +check("zoo run 10: comboI resolves in the source's NEW namespace (slice 2, levels X, Y, Z)", + len(dep10) == 1 and str(dep10[0]["slice"]) == "2" and str(dep10[0]["values"]).startswith("X, Y, Z"), json.dumps(dep10)) +fl10 = {r["flow"]: r for r in table_rows(find_table(zres10, "Option flows"))} +check("zoo run 10: flow row observed the flipped source list and selection", + "[Condition]" in str(fl10.get("dependent dropdown", {}).get("observed")) + and '"Condition" -> slice 2' in str(fl10.get("dependent dropdown", {}).get("observed")), json.dumps(fl10)) +auditz10 = provenance_audit(zres10) +check("zoo run 10: PROVENANCE_AUDIT clean", auditz10 == ("false", "0"), str(auditz10)) +no_raw_tokens(zres10, "zoo run 10") + +# --- zoo run 11 (FLOW F2a): gate closed -> the gated sub-result must NOT exist --------------------- +zr11, zres11 = zoo_run(zoo_full_opts( + host_a="1", host_d="1", host_f="1", host_g="2", host_j="1", host_k="1", + assigned_a={"types": ["scale"], "value": ["Score"]}, + assigned_k={"types": ["nominal"], "value": ["Group"]}, + factors_e=[{"name": "Group", "levels": ["A", "B"]}], + combo_a="Score", combo_i="Score", text_h="hello zoo", rows_h=4, double_h=2.0, slider_h=0.3, + radio_h="sum", gate_h=False, gated_h=12.0), "run 11 (F2a: gate closed)") +check("zoo run 11: NO 'Gated sub-result' table while gateH is off (a gate means absence)", + find_table(zres11, "Gated sub-result") is None, + str([t.get("title") for t in tables(zres11.get("results", zres11)).values()])) +se11 = {r["option"]: r for r in table_rows(find_table(zres11, "Scalar & simple control echoes"))} +check("zoo run 11: gatedH still RECEIVED but annotated 'gate closed'", + "gate closed" in str(se11.get("gatedH", {}).get("received")), json.dumps(se11)) +fl11 = {r["flow"]: r for r in table_rows(find_table(zres11, "Option flows"))} +check("zoo run 11: gate flow row says closed -> no gated sub-result", + "gate closed" in str(fl11.get("checkbox gate", {}).get("observed")), json.dumps(fl11)) +no_raw_tokens(zres11, "zoo run 11") + +# --- zoo run 12 (FLOW F2b): gate open with a different threshold -> numbers move -------------------- +zr12, zres12 = zoo_run(zoo_full_opts( + host_a="1", host_d="1", host_f="1", host_g="2", host_j="1", host_k="1", + assigned_a={"types": ["scale"], "value": ["Score"]}, + assigned_k={"types": ["nominal", "scale"], "value": ["Group", "Age"]}, + factors_e=[{"name": "Group", "levels": ["A", "B"]}], + factors_k=[{"name": "Fac1", "title": "Factor 1", "indicators": {"value": ["Age"], "types": [["scale"]]}}], + combo_a="Score", combo_i="Score", text_h="hello zoo", rows_h=4, double_h=2.0, slider_h=0.3, + radio_h="sum", gate_h=True, gated_h=40.0), "run 12 (F2b: gate open, threshold 40)") +g12 = table_rows(find_table(zres12, "Gated sub-result")) +g12_map = _gated_map(g12) +_a1_above = [a for a in A["Age"] if a > 40] +check("zoo run 12: threshold 40 in the title", "40" in find_table(zres12, "Gated sub-result")["title"], json.dumps(g12)) +check("zoo run 12: gated Age@1 n=%d mean %s (threshold moved the numbers)" % (len(_a1_above), fmt(mean(_a1_above))), + g12_map.get(("1", "Age"), {}).get("nAbove") == len(_a1_above) + and abs((g12_map.get(("1", "Age"), {}).get("meanAbove") or -999) - mean(_a1_above)) < 5e-3, json.dumps(g12)) +check("zoo run 12: NO slice-2 row (its factor-term Age arrives ordinal/factor; nothing numeric survives the threshold)", + all(not str(r["slice"]).startswith("2") for r in g12), json.dumps(g12)) +no_raw_tokens(zres12, "zoo run 12") + +# --- zoo run 13/14 (FLOW F3): the integer field drives the shown row count -------------------------- +zr13, zres13 = zoo_run(zoo_full_opts( + host_a="1", host_d="1", host_f="1", host_g="2", host_j="1", host_k="1", + assigned_a={"types": ["scale"], "value": ["Score"]}, + assigned_k={"types": ["nominal"], "value": ["Group"]}, + factors_e=[{"name": "Group", "levels": ["A", "B"]}], + combo_a="Score", combo_i="Score", text_h="hello zoo", rows_h=3, double_h=2.0, slider_h=0.3, + radio_h="sum", gate_h=True, gated_h=12.0), "run 13 (F3: rowsH=3)") +pv13 = find_table(zres13, "Loaded data - DataSet 1") +check("zoo run 13: preview title says 'first 3 rows'", pv13 is not None and "first 3 rows" in pv13["title"], + pv13["title"] if pv13 else None) +check("zoo run 13: preview DATA has 3 rows", pv13 is not None and len(table_rows(pv13)) == 3) +tv13 = find(z13 := zoo_rows(zres13), "tableview") +check("zoo run 13: Zoo L table shrank to 3 rows (the same integer drives it)", + len(tv13) == 1 and "3 rows x 2 cols" in tv13[0]["received"], json.dumps(tv13)) +fl13 = {r["flow"]: r for r in table_rows(find_table(zres13, "Option flows"))} +check("zoo run 13: rows flow observed rowsH=3", "rowsH=3" in str(fl13.get("number field -> shown rows", {}).get("observed")), json.dumps(fl13)) +no_raw_tokens(zres13, "zoo run 13") + +zr14, zres14 = zoo_run(zoo_full_opts( + host_a="1", host_d="1", host_f="1", host_g="2", host_j="1", host_k="1", + assigned_a={"types": ["scale"], "value": ["Score"]}, + assigned_k={"types": ["nominal"], "value": ["Group"]}, + factors_e=[{"name": "Group", "levels": ["A", "B"]}], + combo_a="Score", combo_i="Score", text_h="hello zoo", rows_h=7, double_h=2.0, slider_h=0.3, + radio_h="sum", gate_h=True, gated_h=12.0), "run 14 (F3: rowsH=7)") +pv14 = find_table(zres14, "Loaded data - DataSet 1") +check("zoo run 14: preview back to 7 rows (both directions of the flow verified)", + pv14 is not None and len(table_rows(pv14)) == 7 and "first 7 rows" in pv14["title"]) +no_raw_tokens(zres14, "zoo run 14") + +# --- zoo run 15 (FLOW F6): the radio group switches the statistic ----------------------------------- +zr15, zres15 = zoo_run(zoo_full_opts( + host_a="1", host_d="1", host_f="1", host_g="2", host_j="1", host_k="1", + assigned_a={"types": ["scale"], "value": ["Score"]}, + assigned_k={"types": ["nominal"], "value": ["Group"]}, + factors_e=[{"name": "Group", "levels": ["A", "B"]}], + combo_a="Score", combo_i="Score", text_h="hello zoo", rows_h=4, double_h=2.0, slider_h=0.3, + radio_h="mean", gate_h=True, gated_h=12.0), "run 15 (F6: radio -> mean)") +cs15 = {r["computation"]: r for r in table_rows(find_table(zres15, "Cross-slice computations"))} +check("zoo run 15: radio-driven statistic switched to mean(Score@1) = 12.50 (was sum 125.00 in run 9)", + "mean 12.50" in str(cs15.get("radio-driven statistic", {}).get("value")), json.dumps(cs15)) +se15 = {r["option"]: r for r in table_rows(find_table(zres15, "Scalar & simple control echoes"))} +check("zoo run 15: radioH echo shows 'mean'", se15.get("radioH", {}).get("received") == "mean", json.dumps(se15)) +no_raw_tokens(zres15, "zoo run 15") + +# --- zoo run 16 (FLOW F4): the variables list drives the interaction terms -------------------------- +zr16, zres16 = zoo_run(zoo_full_opts( + host_a="1", host_d="1", host_f="1", host_g="2", host_j="1", host_k="1", + assigned_a={"types": ["scale"], "value": ["Score"]}, + assigned_k={"types": ["nominal"], "value": ["Group"]}, + factors_e=[{"name": "Group", "levels": ["A", "B"]}], + interaction_d={"types": [["scale"]], "value": [{"components": ["Score"]}], "optionKey": "components"}, + combo_a="Score", combo_i="Score", text_h="hello zoo", rows_h=4, double_h=2.0, slider_h=0.3, + radio_h="sum", gate_h=True, gated_h=12.0), "run 16 (F4: interaction reduced to a single component)") +cs16 = table_rows(find_table(zres16, "Cross-slice computations")) +check("zoo run 16: interaction-product row GONE after the list changed (single-component terms make no product)", + all(r["computation"] != "interaction product" for r in cs16), json.dumps(cs16)) +itr16 = find(zoo_rows(zres16), "interaction-list") +check("zoo run 16: the provenance echo still shows the received term (order=1)", + len(itr16) == 1 and "order=1" in itr16[0]["rowChild"], json.dumps(itr16)) +fl16 = {r["flow"]: r for r in table_rows(find_table(zres16, "Option flows"))} +check("zoo run 16: flow row observed '1 term(s) received'", + "1 term(s) received" in str(fl16.get("variables list -> interaction terms", {}).get("observed")), json.dumps(fl16)) +no_raw_tokens(zres16, "zoo run 16") + +# --- zoo run 17 (FLOW F5): tableL cells are USED, not just echoed ------------------------------------ +zr17, zres17 = zoo_run(zoo_full_opts( + host_a="1", host_d="1", host_f="1", host_g="2", host_j="1", host_k="1", + assigned_a={"types": ["scale"], "value": ["Score"]}, + assigned_k={"types": ["nominal"], "value": ["Group"]}, + factors_e=[{"name": "Group", "levels": ["A", "B"]}], + combo_a="Score", combo_i="Score", text_h="hello zoo", rows_h=4, double_h=2.0, slider_h=0.3, + radio_h="sum", gate_h=True, gated_h=12.0, + table_l=[{"name": "Multiplier", "levels": ["r1", "r2", "r3", "r4"], "values": [2, 2, 2, 2]}, + {"name": "Offset", "levels": ["r1", "r2", "r3", "r4"], "values": [0, 0, 0, 0]}]), + "run 17 (F5: explicit tableL multipliers)") +tv17 = find(zoo_rows(zres17), "tableview") +check("zoo run 17: tableL echo shows the SENT cells (2, 2, 2, 2)", + len(tv17) == 1 and "2, 2, 2, 2" in tv17[0]["rowChild"], json.dumps(tv17)) +cs17 = {r["computation"]: r for r in table_rows(find_table(zres17, "Cross-slice computations"))} +check("zoo run 17: multiplier mean 2.00 x mean(Age@1)=38.00 -> scaled mean 76.000 (cells entered the computation)", + "scaled mean 76.000" in str(cs17.get("tableview multipliers", {}).get("value")), json.dumps(cs17)) +no_raw_tokens(zres17, "zoo run 17") + +# --- zoo run 18 (REGRESSION): nominal factor term in a namespace lacking the column -- +# Pre-fix this FATALed the whole run (rbridge_readDataSetRequested: "gets 'Group.nominal' but its +# not a columnname at all"): the FactorsForm panel is hostless, its term stamped at the ANALYSIS +# level (DataSet 2, which has no Group), and the collector queued a column that dataset does not +# have. The collector now refuses to queue nonexistent columns. Since the per-factor sidecar +# (FactorsFormBase::createFilterIds) transports the term's OWN filterId, the term +# no longer falls back to the analysis-level namespace at all: it encodes through its source +# host's view (filter 1, DataSet 1, which HAS the nominal Group) and resolves in slice 1 - the +# phantom-namespace route is gone. The run completes with a RESOLVED row and a clean audit; the +# existence guard itself stays pinned C++-side (Common/columnencoder.cpp). +zr18, zres18 = zoo_run(zoo_full_opts( + host_a="1", host_d="1", host_f="1", host_g="2", host_j="1", host_k="1", + assigned_a={"types": ["scale"], "value": ["Score"]}, + assigned_k={"types": ["nominal"], "value": ["Group"]}, + factors_e=[{"name": "Group", "levels": ["A", "B"]}], + factors_k=[{"name": "Fac1", "title": "Factor 1", "indicators": {"value": ["Group"], "types": [["nominal"]]}}], + combo_a="Score", combo_i="Score", text_h="hello zoo", rows_h=4, double_h=2.0, slider_h=0.3, + radio_h="sum", gate_h=True, gated_h=12.0), "run 18 (regression: nominal factor term, namespace lacks the column)") +check("zoo run 18: the previously-fatal run COMPLETES", zr18.get("status") == "complete", str(zr18.get("status"))) +fk18 = find(zoo_rows(zres18), "factors-form") +check("zoo run 18: the factor's Group term resolves in ITS OWN term filter's namespace (slice 1) - the phantom-namespace route is gone with the per-factor sidecar", + len(fk18) == 1 and "Group" in fk18[0]["rowChild"] and str(fk18[0]["slice"]) == "1" and "slices: 1" in str(fk18[0]["values"]), json.dumps(fk18)) +check("zoo run 18: the resolution is TERM-level inside the factor object (rowChild carries the per-term slice key 'Group(1)')", + len(fk18) == 1 and "vars: Group(1)" in str(fk18[0]["rowChild"]), json.dumps(fk18)) +auditz18 = provenance_audit(zres18) +check("zoo run 18: PROVENANCE_AUDIT clean after the regression case", auditz18 == ("false", "0"), str(auditz18)) +no_raw_tokens(zres18, "zoo run 18") + +# ---------------------------------------------------------------------------- +# zoo correction - the three pools, the ds1/f1 -> ds1/f3 ANCHOR and the +# derived pool. Zoo M holds three pools over the same test dataset: the hostless pool +# (no host selection, participation key -1), Zoo A's pool on the existing selection +# (dataSetA) and the EXTRA SOURCE zooPoolThree on the bootstrapped third filter +# (dataSetM3 = F3). f3-origin terms carry the filter-3 stamp (JASPColumn_D1_F3_). +# ---------------------------------------------------------------------------- + +# --- zoo run 19: the three pools populated ---------------------------------------------------------- +# assignedThree Score@3 -> the f3 namespace -> slice F3 (12.50: filter 3 is all-rows on ds1) +# assignedDefault bare Score -> the analysis-level fallback (shown = DataSet 2) -> slice 2 +# assignedA is pre-loaded with the anchor's three-term state: Score@1 (f1-origin), Score@2 +# (the ds2 twin) and Age@3 (an f3-ORIGIN term - the one a dataset-scoped sweep would kill). +zr19, zres19 = zoo_run(zoo_full_opts( + host_a="1", host_m3=F3, + assigned_a={"value": [{"name": "Score", "types": ["scale"], "filterId": 1}, + {"name": "Score", "types": ["scale"], "filterId": 2}, + {"name": "Age", "types": ["scale"], "filterId": int(F3)}]}, + assigned_three={"value": [{"name": "Score", "types": ["scale"], "filterId": int(F3)}]}, + assigned_default={"types": ["scale"], "value": ["Score"]}), "run 19 (three pools: f3 pool + default-provider pool + anchor pre-state)") +z19 = zoo_rows(zres19) +f3row = find(z19, "pool-f3") +check("zoo run 19: assignedThree Score@3 resolves in the f3 namespace (slice %s, 12.50)" % F3, + len(f3row) == 1 and str(f3row[0]["slice"]) == F3 and "12.50" in str(f3row[0]["values"]), json.dumps(f3row)) +hl = find(z19, "pool-hostless") +check("zoo run 19: assignedDefault bare Score resolves via the analysis fallback (slice 2, 17.73)", + len(hl) == 1 and str(hl[0]["slice"]) == "2" and "17.73" in str(hl[0]["values"]), json.dumps(hl)) +a19 = find(z19, "assigned-list") +_a19_by = {(r["received"], str(r["slice"])): r for r in a19} +check("zoo run 19: assignedA holds the three-term anchor pre-state (Score@1 12.50, Score@2 17.73, Age@3 as its ordinal values)", + len(a19) == 3 + and "12.50" in str(_a19_by.get(("Score", "1"), {}).get("values")) + and "17.73" in str(_a19_by.get(("Score", "2"), {}).get("values")) + and _age_listing in str(_a19_by.get(("Age", F3), {}).get("values")), json.dumps(a19)) +auditz19 = provenance_audit(zres19) +check("zoo run 19: PROVENANCE_AUDIT clean", auditz19 == ("false", "0"), str(auditz19)) +no_raw_tokens(zres19, "zoo run 19") + +# --- zoo run 20 (THE ANCHOR): switch Zoo A's pool ds1/f1 -> ds1/f3, assignedA NOT re-sent ------------ +# Delta run (only dataSetA changes, exactly like run 8's pattern): the pool's universe becomes +# filter 3's view and availableTermsReset fires with filterIdToUpdate = 1 (the OLD selection - +# the participation key). Expected per the origin-authoritative rule: +# Score@1 participates (1 == 1) -> replaced by the delivered universe term Score@3 (slice F3) +# Score@2 does not (2 != 1) -> survives untouched (slice 2, 17.73) +# Age@3 does not (3 != 1) -> SURVIVES: the anchor. Both filters sit on the SAME dataset, +# so a dataset-scoped participation would have swept it. +zr20, zres20 = zoo_run({"dataSetA": "3", "showMeta": True}, "run 20 (ANCHOR: host A f1->f3, terms not re-sent)") +z20 = zoo_rows(zres20) +a20 = find(z20, "assigned-list") +_a20_by = {(r["received"], str(r["slice"])): r for r in a20} +check("zoo run 20: ANCHOR - Age@3 (f3-origin) NOT swept by the f1->f3 switch (still present, slice %s, ordinal values)" % F3, + ("Age", F3) in _a20_by and _age_listing in str(_a20_by[("Age", F3)].get("values")), json.dumps(a20)) +check("zoo run 20: ANCHOR - Score@2 (foreign stamp) still present (slice 2, 17.73)", + ("Score", "2") in _a20_by and "17.73" in str(_a20_by[("Score", "2")].get("values")), json.dumps(a20)) +check("zoo run 20: Score@1 FOLLOWED the switch (re-stamped to f3: now slice %s, 12.50)" % F3, + ("Score", F3) in _a20_by and "12.50" in str(_a20_by[("Score", F3)].get("values")) + and ("Score", "1") not in _a20_by, json.dumps(a20)) +check("zoo run 20: exactly 3 assignedA rows (one moved, two survived)", + len(a20) == 3, json.dumps(a20)) +check("zoo run 20: dataSetIds still just the workspace's two datasets (filter %s is dataset 1's)" % F3, + sorted(zr20.get("dataSetIds", [])) == [1, 2], str(zr20.get("dataSetIds"))) +auditz20 = provenance_audit(zres20) +check("zoo run 20: PROVENANCE_AUDIT clean after the anchor switch", auditz20 == ("false", "0"), str(auditz20)) +no_raw_tokens(zres20, "zoo run 20") + +# --- zoo run 21: the derived pool (mixed provenance) ------------------------------------------------- +# Zoo N's pool sources assignedA + assignedDefault; its companion assignedDerived receives refs with +# MIXED stamps. Each ref must encode through its OWN stamp (no host of its own): Score@1 in +# slice 1, Condition@2 in slice 2 - one list, two truths. +zr21, zres21 = zoo_run(zoo_full_opts( + host_a="1", + assigned_a={"types": ["scale"], "value": ["Score"]}, + assigned_default={"types": ["nominal"], "value": ["Condition"]}, + assigned_derived={"value": [{"name": "Score", "types": ["scale"], "filterId": 1}, + {"name": "Condition", "types": ["nominal"], "filterId": 2}]}), + "run 21 (derived pool: mixed-provenance refs)") +z21 = zoo_rows(zres21) +dv = find(z21, "derived-pool") +_dv_by = {r["received"]: r for r in dv} +check("zoo run 21: derived ref Score@1 resolves in slice 1 (12.50)", + len(dv) == 2 and "12.50" in str(_dv_by.get("Score", {}).get("values")) + and str(_dv_by.get("Score", {}).get("slice")) == "1", json.dumps(dv)) +check("zoo run 21: derived ref Condition@2 resolves in slice 2 (levels X, Y, Z)", + "X, Y, Z" in str(_dv_by.get("Condition", {}).get("values")) + and str(_dv_by.get("Condition", {}).get("slice")) == "2", json.dumps(dv)) +auditz21 = provenance_audit(zres21) +check("zoo run 21: PROVENANCE_AUDIT clean", auditz21 == ("false", "0"), str(auditz21)) +no_raw_tokens(zres21, "zoo run 21") + +print("\n" + ("ALL CHECKS PASSED" if not fails else f"{len(fails)} FAILURES: {fails}")) +sys.exit(0 if not fails else 1) diff --git a/inst/Description.qml b/inst/Description.qml index a6b066d..ba179fd 100644 --- a/inst/Description.qml +++ b/inst/Description.qml @@ -8,7 +8,7 @@ Description icon: "test.svg" description: qsTr("A module to test stuff with.") requiresData: false - hasWrappers: false + hasWrappers: true preloadData: false Timer @@ -18,7 +18,6 @@ Description { console.log(qsTr("Timer that displays the fact that updating Description.qml from live javascript code works fine.")) testModuleDescription.title = qsTr("Test Module?") - footnotesEntry.title = qsTr("Test Footnotes jaspTable") } running: true @@ -26,13 +25,6 @@ Description } GroupTitle { title: "jaspResults stuff" } - Analysis - { - title: qsTr("Test Transposed jaspTable") - qml: "test.qml" - func: "testTransposedTableFunc" - } - Analysis { title: qsTr("Test jaspTable with crappy data") @@ -48,14 +40,6 @@ Description func: "testTableFormattingFunc" } - Analysis - { - id: footnotesEntry - title: qsTr("Wait for it...") - qml: "test.qml" - func: "testFootnotesTableFunc" - } - Analysis { title: qsTr("Test jaspState") @@ -79,33 +63,58 @@ Description Analysis { - title: qsTr("Test jaspContainer") - qml: "test.qml" - func: "testContainerFunc" + title: qsTr("Test Variables encoding") + qml: "testEncoding.qml" + func: "testEncodingFunc" } Analysis { - title: qsTr("Test Plot Status") - qml: "TestPlotStatus.qml" - func: "testPlotStatus" + title: qsTr("Test Variables encoding with preloadData") + qml: "testEncoding.qml" + func: "testEncodingPreloadDataFunc" + preloadData : true } + Separator {} + + GroupTitle { title: "Multi dataset" } + + // multiDataSetAware is not declared any more: the Analysis finds out by itself once its form + // loads and a control registers a dataset selection (dataSetSelectionOption - see + // testMultiDataSet.qml). The analysis then gets every dataset its options reference as the + // `datasets` argument (jaspBase >= the multiDataSetAware branch) and its FilterSelect dropdowns + // become live. The expander's filter button selects the default (hostless) source. Analysis { - title: qsTr("Test Variables encoding") - qml: "testEncoding.qml" - func: "testEncodingFunc" + title: "Test Multi-DataSet" + qml: "testMultiDataSet.qml" + func: "multiDataSetFunc" + requiresData: true + preloadData: true + hasWrapper: true } + // the component zoo under multi-source provenance. Every control family + // (assigned list, list-as-host, row-control ComponentsList with same-name twins across filters, + // interaction terms, factor levels via encodeThis, textarea with own host, cross-host typed + // bindTo copy) reports what it received and where it resolved; the RPC verifier asserts it. Analysis { - title: qsTr("Test Variables encoding with preloadData") - qml: "testEncoding.qml" - func: "testEncodingPreloadDataFunc" - preloadData : true + title: "Test Multi-DataSet Controls" + qml: "testMultiDataSetControls.qml" + func: "multiDataSetControlsFunc" + requiresData: true + preloadData: true + hasWrapper: true } + // The old non-aware twin entry (func multiDataSetNonAware / qml testMultiDataSetNonAware) + // is gone: it existed only to show that selection options stay inert + // without multiDataSetAware, but its generated wrapper pointed at the AWARE form and so its + // R entry point always errored. The non-aware code path is covered by the other + // (plain, single-dataset) analyses in this module. + Analysis { title: qsTr("Test optionContainsValue") diff --git a/inst/qml/testMultiDataSet.qml b/inst/qml/testMultiDataSet.qml new file mode 100644 index 0000000..a73f22a --- /dev/null +++ b/inst/qml/testMultiDataSet.qml @@ -0,0 +1,82 @@ +import QtQuick +import JASP.Controls +import "./common" + +Form +{ + columns: 1 + + ExplanationText + { + text: + "Test the multi-dataset awareness of the variable lists: each selecting list owns its " + + "dataset/filter selection (only live for multiDataSetAware analyses) and the results " + + "show per-variable routing to the dataset it came from.<br>" + + "<ul>" + + "<li>Open a second dataset (e.g. add another CSV) so the selections have something to choose from.</li>" + + "<li>Set section A's Data selection (on its available list) to one dataset and section B's " + + "to the other: the run should then load <i>both</i> datasets (see the 'Datasets in this " + + "run' table), and the assigned lists inherit their available list's namespace.</li>" + + "<li>Drag variables (try the identically named 'Score'!) into both lists: the routing table " + + "must attribute each one to its own dataset, filter and column.</li>" + + "<li>show .meta: the provenance stamps (dataSetId/filterId) on the bound options.</li>" + + "<li>guard demo: readDataSet() must hard-stop inside an aware run.</li>" + + "<li>Save the analysis to a .jasp and reopen it: the dataset routing must survive the round trip.</li>" + + "</ul>" + + "If no Data selection shows up, the results table will say 'Non-aware run': the analysis was " + + "not recognised as multiDataSetAware (old JASP binary or stale module install)." + } + + Section { + title: qsTr("Form A - variables from one dataset") + + VariablesForm + { + AvailableVariablesList + { + name: "allVariablesA" + //List-level dataset selection: an option carrying the dataset name + //to select for this list (jaspSyntax gates the bound dropdown on it; desktop never + //sees it). The assigned lists below inherit this list's namespace. + dataSetSelectionOption: "dataSetA" + } + AssignedVariablesList + { + name: "dependentA" + title: qsTr("Variables A") + depends: "dataSetA" //bind the dataset selection first, then the columns + } + } + } + + Section { + title: qsTr("Form B - variables from (hopefully) another dataset") + + VariablesForm + { + AvailableVariablesList + { + name: "allVariablesB" + dataSetSelectionOption: "dataSetB" + } + AssignedVariablesList + { + name: "dependentB" + title: qsTr("Variables B") + depends: "dataSetB" + } + } + } + + CheckBox + { + name: "showMeta" + label: qsTr("Show options .meta (dataSetId/filterId provenance)") + } + + CheckBox + { + name: "guardDemo" + label: qsTr("Call readDataSet() anyway (should error in aware runs)") + } +} diff --git a/inst/qml/testMultiDataSetControls.qml b/inst/qml/testMultiDataSetControls.qml new file mode 100644 index 0000000..ba2bc43 --- /dev/null +++ b/inst/qml/testMultiDataSetControls.qml @@ -0,0 +1,537 @@ +// the component zoo under multi-source provenance. +// +// Every control family a real analysis form uses, each exercising per-item provenance: +// Zoo A plain lists: an AvailableVariablesList carrying its own dataset/filter selection +// (the list-level pattern) feeding an AssignedVariablesList, plus a DropDown +// served by the same host (comboboxes stay PLAIN strings - never term objects - +// while their value still resolves through the host namespace). +// Zoo B REMOVED (zoo correction, owner): a bare assigned list - an assigned +// list with no source at all - is a malformed configuration, not a supported one: +// every real assigned list sits in a form with a pool. Its two real pins moved: the +// -1 analysis-fallback resolution is pinned on the default-provider pool's assigned +// list (Zoo M, verifier run 19), foreign-stamp survival on the anchor runs. +// Zoo C TableFields-style ROW-CONTROL list: JASP.TableFields does not exist in this +// codebase and ComponentsList (its closest named relative) cannot carry a selection, +// so this is the row family as TableFields actually is it: an AssignedVariablesList +// with a rowComponent (a keep checkbox and a shift integer), in its own form over a +// default-provider pool (every assigned list has >= 1 source - owner rule). Same-name +// twin rows behind DIFFERENT filters (the same-name twins, set by the RPC verifier through +// the Storage term-object shape) must keep their own children - the +// RelatedValuesPerTerm/RowControls re-keying (TermRowKey = value + filterId) is what +// makes that work; pre-fix the second row silently clobbered the first. The pool's +// key is -1, so the stamped twins are never swept by a universe reset: each twin +// follows only its own stamp. +// Zoo D interaction terms: a VariablesList in Interaction mode - components of one +// combination term share the term-level filterId (uniform-carry rule). +// Zoo E FactorLevelList: encodeThis -> extras-encoder roundtrip (factor/level names encode +// through the JaspExtraOptions_ namespace and must come back decoded and usable). +// Zoo F TextArea with its OWN host: R-code text whose used columns encode through the +// selected filter's view (host-bound resolution). +// Zoo G CROSS-HOST bindTo: a list with its own selection whose option is bound +// (bindTo = the JSON->control applier) to a typed copy generated from Zoo A's list - +// the copy's terms carry the SOURCE filterId. The destination rule stamps only +// UNQUALIFIED terms (the intake guard) and the restamp walk never touches foreign +// stamps (owns-the-stamp), so the value must encode and resolve through the +// SOURCE's slice even though this list's own dropdown selects another one. +// Zoo M THREE pools over the same test dataset (the zoo correction): a +// hostless pool (no dataSetSelectionOption at all - the default-provider pool whose +// participation key is -1), Zoo A's pool hosted by the existing filter selection +// (dataSetA, filter 1), and the EXTRA SOURCE: a new AvailableVariablesList hosted by +// the THIRD filter of the same test dataset. The third filter does not exist in a +// fresh RPC workspace (data_load mints one default filter per dataset and the RPC has +// no filter-creation method), so this form bootstraps it itself - see the guard in +// Zoo M (module-side test fixture: the test provides its own filters). f3-origin terms +// carry its stamp, which is what makes the anchor scenario assertable: switching Zoo +// A's pool from ds1/f1 to ds1/f3 must move the f1-stamped terms (their participation +// key is the OLD selection id) while an f3-origin term in the same list survives +// untouched - a dataset-scoped sweep would wrongly have swept it, because both +// filters sit on the SAME dataset. +// Zoo N DERIVED pool: an AvailableVariablesList whose source is TWO assigned lists +// (assignedA + assignedDefault, mixed provenance). Its terms are references that keep +// each source term's OWN stamp; the known limitation (the reason the `to` scalar was +// dropped from the reset signal) is that mixed-source refs do not follow a +// source-list host switch this pass. +// +// The R echo (R/multiDataSetControls.R) reports for every received value: the (encoded) name +// as received, WHICH slice index it resolved in, and the row children - so provenance is +// assertable from the results through Tools/verifyMultiDataSetRpc.py. +import QtQuick +import JASP +import JASP.Controls +import "./common" + +Form +{ + columns: 1 + + ExplanationText + { + text: + "Component-zoo provenance test (multiDataSetAware). Open two datasets, then check the " + + "'Controls provenance' table: every control family lists the values it received, the " + + "slice each value resolved in and its row children.<br>" + + "<ul>" + + "<li>Zoo A: selection on the available list drives the assigned list and the combobox.</li>" + + "<li>Zoo B: removed - a bare assigned list (no source) is malformed; its pins live on the " + + "default-provider pool's list (Zoo M).</li>" + + "<li>Zoo C: row-control list (row-bearing variables list, the TableFields shape), sourced " + + "from the default-provider pool: same-name twin rows " + + "from two filters keep their OWN row values.</li>" + + "<li>Zoo D: interaction terms share one filterId over all components.</li>" + + "<li>Zoo E: factor/level names roundtrip through the extras encoder.</li>" + + "<li>Zoo F: the R-code textarea encodes its tokens through its own host.</li>" + + "<li>Zoo G: a typed copy bound across differently-selected hosts keeps the SOURCE filterId.</li>" + + "<li>Zoo H: scalar families (text / integer / real / slider / radio / checkbox-gated field) echo " + + "as plain values AND drive results: the integer sets the preview row count, the radio picks the " + + "flow statistic, the checkbox gates a sub-result.</li>" + + "<li>Zoo I: a DEPENDENT dropdown - its option list is driven by Zoo A's assigned list.</li>" + + "<li>Zoo J: a paired variables list over the pool on dataSetJ; the pair is one term over two columns.</li>" + + "<li>Zoo K: a FactorsForm (the FactorsList family) fed by an assigned list over the pool on dataSetK.</li>" + + "<li>Zoo L: a bound TableView whose row count follows Zoo H's integer field.</li>" + + "<li>Zoo M: three pools over DataSet 1 - hostless (-1), Zoo A's pool on filter 1, and the extra source on the third filter; switching Zoo A from f1 to f3 must not sweep an f3-origin term.</li>" + + "<li>Zoo N: a derived pool whose options come from Zoo A's and the default pool's assigned lists (mixed provenance).</li>" + + "</ul>" + + "The results panel shows the LOADED DATA per slice (head of every column, row count = Zoo H's " + + "integer), the per-family provenance echo, the scalar echoes, the flow demonstrations and the " + + "cross-slice computations." + + "'Show options .meta' adds the PROVENANCE_AUDIT (R must never see provenance)." + } + + // ---- Zoo A: list-level selection + combobox ---------------------------------- + Section { + title: qsTr("Zoo A - plain lists, own selection") + + VariablesForm + { + AvailableVariablesList + { + name: "zooVarsA" + dataSetSelectionOption: "dataSetA" + } + AssignedVariablesList + { + name: "assignedA" + title: qsTr("Assigned A") + depends: "dataSetA" + } + // a combobox served by the same host: values stay PLAIN strings (never term objects) + // while the selection resolves through the host namespace (landmine: no + // term-objectification of combos). NB: the source must be the STRING NAME of the + // control - a bare `source: zooVarsA` is a QML ReferenceError (the id does not exist; + // found via the runtime log: "ReferenceError: zooVarsA is not defined") and the + // dropdown silently ran with an empty option list until fixed. + DropDown + { + name: "comboA" + label: qsTr("Column combo (options from the available list)") + source: "zooVarsA" + addEmptyValue: true + } + } + } + + // ---- Zoo C: TableFields-style row controls -------------------------------------------------- + // JASP.TableFields does not exist in this codebase; ComponentsList is its closest NAMED + // relative but is not a VariablesListBase. The row machinery RowControls/TermRowKey is shared + // with the row-bearing variables list, which is also what TableFields actually is (term rows + + // per-row controls) - so the zoo exercises the row family through an AssignedVariablesList + // with a rowComponent, in its own form over a default-provider pool (every assigned list has + // >= 1 source - owner rule; the bare standalone form died with Zoo B). Its pool's key is -1, + // so the stamped twin rows below are never swept by any reset - each twin follows only its + // own stamp, which is exactly the same-name-twins case. + Section { + title: qsTr("Zoo C - row controls: per-row term + per-row values (default-provider source)") + + VariablesForm + { + AvailableVariablesList + { + name: "zooPoolC" // default-provider pool: no selection, participation key -1 + } + AssignedVariablesList + { + name: "rowsC" + title: qsTr("Rows (term + keep + shift)") + optionKey: "variable" + + rowComponent: Row + { + CheckBox + { + name: "keep" + } + IntegerField + { + name: "shift" + min: -999 + max: 999 + } + } + } + } + } + + // ---- Zoo D: interaction terms ---------------------------------------------------------------- + Section { + title: qsTr("Zoo D - interaction terms (components share one filterId)") + + VariablesForm + { + AvailableVariablesList + { + name: "zooVarsD" + dataSetSelectionOption: "dataSetD" + } + VariablesList + { + name: "interactionD" + title: qsTr("Model terms") + listViewType: JASP.Interaction + depends: "dataSetD" + } + // (no CrossButton/AssignButton chrome needed for the test: the verifier drives + // interactionD through its option; the buttons are pure UI in this codebase) + } + } + + // ---- Zoo E: factor/level names (encodeThis -> extras) --------------------------------------- + Section { + title: qsTr("Zoo E - factor levels (extras roundtrip)") + + FactorLevelList + { + name: "factorsE" + minFactors: 0 + minLevels: 0 + factorName: "Fac" + levelName: "Lev" + } + } + + // ---- Zoo F: textarea with its own host ------------------------------------------------------- + Section { + title: qsTr("Zoo F - R-code textarea, own host") + + TextArea + { + name: "codeF" + dataSetSelectionOption: "dataSetF" + depends: "dataSetF" // the host binds first: the bind-time encodeRScript resolves THIS filter, not the analysis fallback + textType: JASP.TextTypeRcode + } + } + + // ---- Zoo G: cross-host bindTo --------------------------------------------------------- + // The selection lives on the POOL now (the per-list dataSetSelectionOption crutch is gone, + // same correction as B/C): boundG sits in a form over the pool hosted by dataSetG's filter, + // so every assigned list has its source. The cross-host pin is unchanged: the verifier binds + // a typed copy of a filter-1 term into boundG while the pool selects filter 2 - the copy + // keeps the SOURCE filterId and encodes through the SOURCE's slice, origin + // -authoritative: a stamp is born at Storage/bindTo and is not requalified by a destination. + Section { + title: qsTr("Zoo G - typed copy bound from Zoo A keeps the SOURCE filter") + + VariablesForm + { + AvailableVariablesList + { + name: "zooPoolG" + dataSetSelectionOption: "dataSetG" + } + AssignedVariablesList + { + name: "boundG" + title: qsTr("Bound from assignedA (typed copy)") + depends: "dataSetG" + } + } + } + + // ---- Zoo H: scalar control families ----------------------------------------------------------- + // Every scalar family a real form binds, each with an R-side consumer that actually USES the + // value (not just echoes it): the integer drives the data-preview row count and Zoo L's row + // count, the radio picks the flow statistic, the checkbox gates a whole sub-result, the real + // and the gated field enter the cross-slice computations. Slider is echo-only (a fraction has + // no sensible computation here - documented in R/multiDataSetControls.R, not faked). + Section { + title: qsTr("Zoo H - scalar controls (text / integer / real / slider / radio / gated)") + + columns: 2 + + TextField + { + name: "textH" + label: qsTr("Text field (echoed verbatim)") + placeholderText: qsTr("echo me") + } + IntegerField + { + id: rowsH + name: "rowsH" + label: qsTr("Preview rows (drives data previews + Zoo L)") + defaultValue: 5 + min: 1 + max: 10 + } + DoubleField + { + name: "doubleH" + label: qsTr("Scale factor (real number)") + defaultValue: 1.5 + min: 0 + max: 100 + decimals: 2 + } + Slider + { + name: "sliderH" + label: qsTr("Slider fraction (echo only)") + value: 0.5 // Slider has no defaultValue property: the initial bound value IS `value` + min: 0 + max: 1 + stepSize: 0.1 + } + RadioButtonGroup + { + name: "radioH" + title: qsTr("Flow statistic") + + RadioButton { value: "mean"; label: qsTr("Mean"); checked: true } + RadioButton { value: "sum"; label: qsTr("Sum") } + } + CheckBox + { + name: "gateH" + label: qsTr("Gate: produce the gated sub-result") + + DoubleField + { + name: "gatedH" + label: qsTr("Gated threshold") + defaultValue: 10.0 + min: 0 + max: 100 + decimals: 1 + } + } + } + + // ---- Zoo I: dependent dropdown ----------------------------------------------------------------- + // The option LIST of this dropdown is driven by another control's value: source is Zoo A's + // assigned list, so its choices are exactly the terms currently in assignedA (and follow every + // change of that list - the RPC verifier flips assignedA and asserts the effective selection + // follows). Value stays a PLAIN string (never term-objectified, landmine 4). + Section { + title: qsTr("Zoo I - dependent dropdown (options driven by Zoo A's assigned list)") + + DropDown + { + name: "comboI" + label: qsTr("Pick a column from Assigned A") + source: "assignedA" + addEmptyValue: true + } + } + + // ---- Zoo J: paired variables list --------------------------------------------------------------- + // The paired-list family: AssignedPairsVariablesList is a VariablesList preset (2 columns, + // drop-replace) whose model PAIRS its columns into 2-component terms (bound value + // [["Score","Age"]]). Served by the pool hosted on dataSetJ's filter, so the pair's + // components stamp together under that pool's filter (uniform carry); the R side echoes + // each pair and computes per pair. + Section { + title: qsTr("Zoo J - paired variables list (pool on dataSetJ)") + + VariablesForm + { + AvailableVariablesList + { + name: "zooPoolJ" + dataSetSelectionOption: "dataSetJ" + } + AssignedPairsVariablesList + { + name: "pairsJ" + title: qsTr("Pairs (pool on dataSetJ)") + depends: "dataSetJ" + } + } + } + + // ---- Zoo K: factors form (the FactorsList family) ------------------------------------------------ + // FactorsList is one panel of a FactorsForm; the serialized shape is the form's: per factor + // {name, title, indicators}. Findings (pinned by the verifier): + // 1. The form's inner panels are NOT hosts, but the per-factor sidecar + // (FactorsFormBase::createFilterIds) transports each term's OWN filterId + // per factor and per term: a term keeps the stamp its source list synced onto it + // (assignedK -> filter 1) and encodes through THAT filter's view - resolved slice by + // slice, no analysis-level fallback (the hostless-panel deferral is CLOSED). + // 2. A factor term must be a member of the form's SOURCE list (assignedK): a term outside it is + // silently dropped by the form's model sync, and a term the form keeps is RE-TYPED by the + // form (Age arrived ordinal here although sent scale, so the engine slice receives it as an + // R factor). A term whose column the resolved namespace lacks used to FATAL the run - the + // existence guard in Common/columnencoder.cpp refuses to queue columns a + // namespace does not have; with the per-factor sidecar the term resolves in its OWN + // namespace instead (verifier run 18 pins the resolved row + clean audit). + Section { + title: qsTr("Zoo K - factors form (FactorsList panels over an assigned source)") + + VariablesForm + { + AvailableVariablesList + { + name: "zooPoolK" + dataSetSelectionOption: "dataSetK" + } + AssignedVariablesList + { + name: "assignedK" + title: qsTr("Assigned K (factor source)") + depends: "dataSetK" + } + } + FactorsForm + { + name: "factorsK" + initNumberFactors: 1 + baseName: "Fac" + baseTitle: "Factor" + allowedColumns: ["scale", "ordinal", "nominal"] + availableVariablesList.source: "assignedK" + } + } + + // ---- Zoo L: bound table view --------------------------------------------------------------------- + // The BoundControlTableView family: a bound SimpleTableView whose ROW COUNT is driven by Zoo + // H's integer field (initialRowCount + tableView.rowCount follow rowsH, exactly the + // testTableView.qml pattern). Its cells are plain values (no provenance - a tableview option + // is not a Terms option) and the R side USES them: the multiplier column rescales the assigned + // columns of every slice in the cross-slice computation. + Section { + title: qsTr("Zoo L - bound table view (row count driven by Zoo H's integer field)") + + SimpleTableView + { + name: "tableL" + cornerText: qsTr("Par") + initialRowCount: rowsH.value + tableView.rowCount: rowsH.value + initialColumnCount: 2 + columnNames: [qsTr("Multiplier"), qsTr("Offset")] + itemTypePerColumn: [JASP.Double, JASP.Double] + + function getDefaultValue(columnIndex, rowIndex) + { + return columnIndex === 0 ? 1.0 + rowIndex : 0.5 * (rowIndex + 1); + } + } + } + + // ---- Zoo M: three pools over one dataset (hostless / f1 / f3) -------------------------------- + // The zoo correction. The EXTRA SOURCE is zooPoolThree: an AvailableVariablesList + // whose host selection is the THIRD filter of the same test dataset. A fresh RPC workspace has + // only the two default filters (data_load mints one per dataset; the RPC has no filter-creation + // method), so the form bootstraps the third filter itself, once, guarded: only when the + // workspace is exactly the zoo fixture shape (a dataset with id 1 that has no "Filter 0" yet). + // filterModel/workspace are root-context properties and module QML loads under the root + // context, so both are visible here. filterModel.addFilter(1) is the same invokable the filter + // editor uses (DataSet::addFilter -> named "Filter 0", default all-rows rFilter, its own + // data-table column and its own encoder namespace JASPColumn_D1_F3_). + // + // The three pools and their participation keys: + // zooPoolHostless no dataSetSelectionOption -> key -1 (default provider) + // zooVarsA (Zoo A) dataSetA -> key = whatever dataSetA holds (f1 -> f3 anchor) + // zooPoolThree dataSetM3 -> key 3 (the f3-origin namespace) + Component.onCompleted: { + // Zoo fixture bootstrap (test-only): ensure the third filter exists on the existing test + // dataset. No-op in any other workspace shape; no-op once it exists (verifier re-runs). + // NB: the 'workspace' ROOT CONTEXT PROPERTY is only set from onWorkspaceChanged() and was + // observed UNDEFINED at form load in an RPC session - route through dataSetPackage's + // workspace Q_PROPERTY instead, which loadQML() always installs. + var ws = (typeof dataSetPackage !== "undefined" && dataSetPackage) ? dataSetPackage.workspace : null + if (typeof filterModel !== "undefined" && ws && ws.filterIdByName(1, "Filter 0") < 0) + console.log("ZOO BOOTSTRAP: creating the third filter on dataset 1 -> id " + + (function(){ filterModel.addFilter(1); return ws.filterIdByName(1, "Filter 0"); })()) + } + + Section { + title: qsTr("Zoo M - the extra source: pool on the third filter (+ the hostless pool)") + + VariablesForm + { + // the default-provider pool: NO host selection, participation key -1 + AvailableVariablesList + { + name: "zooPoolHostless" + } + AssignedVariablesList + { + name: "assignedDefault" + title: qsTr("Assigned (default-provider pool)") + } + } + VariablesForm + { + // THE EXTRA SOURCE: hosted by the third filter of the same test dataset; its universe + // terms carry the f3 stamp (JASPColumn_D1_F3_ namespace) + AvailableVariablesList + { + name: "zooPoolThree" + dataSetSelectionOption: "dataSetM3" + } + AssignedVariablesList + { + name: "assignedThree" + title: qsTr("Assigned (third-filter pool)") + } + } + } + + // ---- Zoo N: derived pool (mixed sources) ------------------------------------------------------ + // An AvailableVariablesList whose source is TWO assigned lists: its listed terms are + // references carrying each source term's OWN stamp (mixed provenance). Known limitation + // (documented in the): mixed-source refs do not follow a source-list host + // switch this pass - that is exactly why the reset signal carries the participation key + // instead of a `to` scalar. + Section { + title: qsTr("Zoo N - derived pool (source: assignedA + assignedDefault)") + + VariablesForm + { + AvailableVariablesList + { + name: "zooPoolDerived" + source: ["assignedA", "assignedDefault"] + } + AssignedVariablesList + { + name: "assignedDerived" + title: qsTr("Assigned (derived pool)") + } + } + } + + // ---- scalars (never provenanced, never term-objectified) ------------------------ + Section { + title: qsTr("Scalars") + + columns: 2 + + CheckBox + { + name: "showMeta" + label: qsTr("Show options .meta (PROVENANCE_AUDIT)") + } + CheckBox + { + name: "plainSwitch" + label: qsTr("Plain switch") + } + DropDown + { + name: "comboG" + label: qsTr("Plain combo") + values: ["first", "second", "third"] + } + } +} diff --git a/renv.lock b/renv.lock index 3b3ac06..5fef58b 100644 --- a/renv.lock +++ b/renv.lock @@ -1,6 +1,6 @@ { "R": { - "Version": "4.5.2", + "Version": "4.6.1", "Repositories": [ { "Name": "CRAN", @@ -27,7 +27,7 @@ }, "Rcpp": { "Package": "Rcpp", - "Version": "1.1.1", + "Version": "1.1.2", "Source": "Repository", "Requirements": [ "methods", @@ -37,7 +37,7 @@ }, "S7": { "Package": "S7", - "Version": "0.2.1", + "Version": "0.2.2", "Source": "Repository", "Requirements": [ "R", @@ -54,7 +54,7 @@ }, "base64enc": { "Package": "base64enc", - "Version": "0.1-3", + "Version": "0.1-6", "Source": "Repository", "Requirements": [ "R" @@ -62,7 +62,7 @@ }, "bslib": { "Package": "bslib", - "Version": "0.9.0", + "Version": "0.12.0", "Source": "Repository", "Requirements": [ "base64enc", @@ -91,9 +91,10 @@ }, "callr": { "Package": "callr", - "Version": "3.7.6", + "Version": "3.8.0", "Source": "Repository", "Requirements": [ + "otel", "processx", "R", "R6", @@ -102,7 +103,7 @@ }, "cli": { "Package": "cli", - "Version": "3.6.5", + "Version": "3.6.6", "Source": "Repository", "Requirements": [ "R", @@ -119,7 +120,7 @@ }, "cpp11": { "Package": "cpp11", - "Version": "0.5.2", + "Version": "0.5.5", "Source": "Repository", "Requirements": [ "R" @@ -138,7 +139,7 @@ }, "curl": { "Package": "curl", - "Version": "7.0.0", + "Version": "8.1.0", "Source": "Repository", "Requirements": [ "R" @@ -146,7 +147,7 @@ }, "data.table": { "Package": "data.table", - "Version": "1.18.0", + "Version": "1.18.6.1", "Source": "Repository", "Requirements": [ "methods", @@ -175,7 +176,7 @@ }, "dplyr": { "Package": "dplyr", - "Version": "1.1.4", + "Version": "1.2.1", "Source": "Repository", "Requirements": [ "cli", @@ -252,7 +253,7 @@ }, "fs": { "Package": "fs", - "Version": "1.6.6", + "Version": "2.1.0", "Source": "Repository", "Requirements": [ "methods", @@ -261,7 +262,7 @@ }, "gdtools": { "Package": "gdtools", - "Version": "0.4.4", + "Version": "0.5.2", "Source": "Repository", "Requirements": [ "fontquiver", @@ -283,7 +284,7 @@ }, "ggplot2": { "Package": "ggplot2", - "Version": "4.0.1", + "Version": "4.0.3", "Source": "Repository", "Requirements": [ "cli", @@ -303,7 +304,7 @@ }, "glue": { "Package": "glue", - "Version": "1.8.0", + "Version": "1.8.1", "Source": "Repository", "Requirements": [ "methods", @@ -312,7 +313,7 @@ }, "gridExtra": { "Package": "gridExtra", - "Version": "2.3", + "Version": "2.3.1", "Source": "Repository", "Requirements": [ "graphics", @@ -348,7 +349,7 @@ }, "highr": { "Package": "highr", - "Version": "0.11", + "Version": "0.12", "Source": "Repository", "Requirements": [ "R", @@ -384,7 +385,7 @@ }, "httr": { "Package": "httr", - "Version": "1.4.7", + "Version": "1.4.9", "Source": "Repository", "Requirements": [ "curl", @@ -436,14 +437,14 @@ "RemoteHost": "api.github.com", "RemoteUsername": "jasp-stats", "RemoteRepo": "jaspBase", - "RemoteSha": "a5770b1bb36f6c5aebca3d15e5eab0dd4976602e" + "RemoteSha": "a1f5d9b740c955f3d9bde8106debdf7293539316" }, "jaspGraphs": { "Package": "jaspGraphs", "Version": "0.20.0", "Source": "GitHub", "Requirements": [ - "testthat", + "R", "cli", "ggplot2", "gridExtra", @@ -454,13 +455,15 @@ "plotly", "R6", "rlang", - "scales" + "scales", + "testthat", + "vdiffr" ], "RemoteType": "github", "RemoteHost": "api.github.com", "RemoteUsername": "jasp-stats", "RemoteRepo": "jaspGraphs", - "RemoteSha": "db3c899263a1d8d236ed0e7e3a7b6814b48c14ae" + "RemoteSha": "288d2751131ee6ac17362bd89d2ed7d9c0c67c7c" }, "jquerylib": { "Package": "jquerylib", @@ -480,7 +483,7 @@ }, "knitr": { "Package": "knitr", - "Version": "1.51", + "Version": "1.52", "Source": "Repository", "Requirements": [ "evaluate", @@ -503,7 +506,7 @@ }, "later": { "Package": "later", - "Version": "1.4.5", + "Version": "1.4.8", "Source": "Repository", "Requirements": [ "R", @@ -513,10 +516,11 @@ }, "lazyeval": { "Package": "lazyeval", - "Version": "0.2.2", + "Version": "0.2.3", "Source": "Repository", "Requirements": [ - "R" + "R", + "rlang" ] }, "lifecycle": { @@ -531,7 +535,7 @@ }, "magrittr": { "Package": "magrittr", - "Version": "2.0.4", + "Version": "2.0.5", "Source": "Repository", "Requirements": [ "R" @@ -556,7 +560,7 @@ }, "officer": { "Package": "officer", - "Version": "0.7.3", + "Version": "0.7.7", "Source": "Repository", "Requirements": [ "cli", @@ -576,7 +580,7 @@ }, "openssl": { "Package": "openssl", - "Version": "2.3.4", + "Version": "2.4.2", "Source": "Repository", "Requirements": [ "askpass" @@ -627,7 +631,7 @@ }, "plotly": { "Package": "plotly", - "Version": "4.11.0", + "Version": "4.12.1", "Source": "Repository", "Requirements": [ "base64enc", @@ -640,7 +644,6 @@ "htmlwidgets", "httr", "jsonlite", - "lazyeval", "magrittr", "promises", "purrr", @@ -666,7 +669,7 @@ }, "processx": { "Package": "processx", - "Version": "3.8.6", + "Version": "3.9.0", "Source": "Repository", "Requirements": [ "ps", @@ -692,7 +695,7 @@ }, "ps": { "Package": "ps", - "Version": "1.9.1", + "Version": "1.9.3", "Source": "Repository", "Requirements": [ "R", @@ -701,7 +704,7 @@ }, "purrr": { "Package": "purrr", - "Version": "1.2.1", + "Version": "1.2.2", "Source": "Repository", "Requirements": [ "cli", @@ -714,7 +717,7 @@ }, "ragg": { "Package": "ragg", - "Version": "1.5.0", + "Version": "1.5.2", "Source": "Repository", "Requirements": [ "systemfonts", @@ -731,7 +734,7 @@ }, "rlang": { "Package": "rlang", - "Version": "1.1.7", + "Version": "1.3.0", "Source": "Repository", "Requirements": [ "R", @@ -740,7 +743,7 @@ }, "rmarkdown": { "Package": "rmarkdown", - "Version": "2.30", + "Version": "2.32", "Source": "Repository", "Requirements": [ "bslib", @@ -761,7 +764,7 @@ }, "rvg": { "Package": "rvg", - "Version": "0.4.0", + "Version": "0.4.2", "Source": "Repository", "Requirements": [ "gdtools", @@ -805,7 +808,7 @@ }, "stringi": { "Package": "stringi", - "Version": "1.8.7", + "Version": "1.8.9", "Source": "Repository", "Requirements": [ "R", @@ -852,7 +855,7 @@ }, "systemfonts": { "Package": "systemfonts", - "Version": "1.3.1", + "Version": "1.3.2", "Source": "Repository", "Requirements": [ "base64enc", @@ -867,7 +870,7 @@ }, "textshaping": { "Package": "textshaping", - "Version": "1.0.4", + "Version": "1.0.5", "Source": "Repository", "Requirements": [ "cpp11", @@ -933,7 +936,7 @@ }, "tinytex": { "Package": "tinytex", - "Version": "0.58", + "Version": "0.61", "Source": "Repository", "Requirements": [ "xfun" @@ -949,7 +952,7 @@ }, "uuid": { "Package": "uuid", - "Version": "1.2-1", + "Version": "1.2-2", "Source": "Repository", "Requirements": [ "R" @@ -957,7 +960,7 @@ }, "vctrs": { "Package": "vctrs", - "Version": "0.7.0", + "Version": "0.7.3", "Source": "Repository", "Requirements": [ "cli", @@ -969,7 +972,7 @@ }, "viridisLite": { "Package": "viridisLite", - "Version": "0.4.2", + "Version": "0.4.3", "Source": "Repository", "Requirements": [ "R" @@ -977,7 +980,7 @@ }, "withr": { "Package": "withr", - "Version": "3.0.2", + "Version": "3.0.3", "Source": "Repository", "Requirements": [ "graphics", @@ -987,7 +990,7 @@ }, "xfun": { "Package": "xfun", - "Version": "0.56", + "Version": "0.62", "Source": "Repository", "Requirements": [ "grDevices", @@ -998,7 +1001,7 @@ }, "xml2": { "Package": "xml2", - "Version": "1.5.2", + "Version": "1.6.0", "Source": "Repository", "Requirements": [ "cli", @@ -1015,9 +1018,11 @@ }, "zip": { "Package": "zip", - "Version": "2.3.3", + "Version": "3.0.2", "Source": "Repository", - "Requirements": [] + "Requirements": [ + "cli" + ] } } }