Migrate local analysis replay to jaspSyntax#79
Conversation
vandenman
left a comment
There was a problem hiding this comment.
Still need to actually run this, but some feedback below. The subprocesses idea looks interesting and has potential, but the current implementation looks a little fragile. Also, subprocesses are nice in theory but can make errors much harder to debug.
| #' | ||
| #' @export analysisOptions | ||
| analysisOptions <- function(source) { | ||
| analysisOptions <- function(source, modulePath = NULL) { |
There was a problem hiding this comment.
Isn't this always clear from context? E.g., when running jaspdescriptives it's clear that that is also always the module path?
There was a problem hiding this comment.
Mostly yes, and the default path still does that inference from module.dirs / the active module context. I kept modulePath as an optional provenance override because .jasp files record module identity/version, not the local source checkout path. That matters for source-branch replay, generated tests, and multi-module archives; normal module workflows should not need to pass it.
|
|
||
| modulePath <- .modulePathForAnalysisName(analysis, modulePath) | ||
| options <- jaspSyntax::readDefaultAnalysisOptions( | ||
| modulePath = modulePath, |
There was a problem hiding this comment.
Or is this the reason for the above? Still think we could infer the module path automatically?
There was a problem hiding this comment.
Yes, this is the reason for the optional argument. The implementation still infers when modulePath = NULL; explicit modulePath only pins the source checkout or disambiguates a named module/analysis path collection. I kept this as orchestration/provenance, not extra QML semantics in jaspTools.
| rFuncLocExpr <- paste0("\\{[^\\{\\}]*func:\\s*", name, "[^\\{\\}]*\\}") | ||
| if (!grepl(rFuncLocExpr, fileContents)) | ||
| stop("Could not locate qml file for R function ", name, " in inst/qml directory and did not find the R function in inst/Description.qml to look for an alternative name for the qml file") | ||
| analysisOptionsFromQMLFileSubprocess <- function(analysis, modulePath = NULL) { |
There was a problem hiding this comment.
Why do we use subprocesses? That feels a little over engineered? I mean not a bad idea to be able to run tests in parallel in the future, but don't think that is your motivation right now?
There was a problem hiding this comment.
Agreed for analysisOptions(). I removed the jaspTools subprocess path there; QML defaults now call jaspSyntax::readDefaultAnalysisOptions() directly. The .jasp extraction isolation remains in jaspSyntax, where the native bridge state is owned.
|
|
||
| return(list(value = value, types = typesStructure)) | ||
| } | ||
| `%||%` <- function(x, y) { |
There was a problem hiding this comment.
This exists in base R already since R 4.5.0 or so, so maybe use that one instead (and definitely avoid shadowing that one otherwise).
There was a problem hiding this comment.
Agreed. I removed the custom %||% helper so we do not shadow base R. The only remaining use case was a label fallback, now handled by a named internal helper.
|
|
||
| .jaspToolsLaunchSubprocess <- function(scriptPath, inputPath, outputPath, logPath) { | ||
| rscript <- file.path(R.home("bin"), if (.Platform$OS.type == "windows") "Rscript.exe" else "Rscript") | ||
| system2( |
There was a problem hiding this comment.
Yeah so if we really want this we should consider mirai or processx rather than doing it "manually".
There was a problem hiding this comment.
Agreed. I replaced the manual Rscript/system2 runner with callr, which uses processx underneath. I kept subprocess containment only for runAnalysis(): while checking this against jaspMixedModels, direct in-process replay of the Larks and Owls GLMM still crashes R with access violation 0xC0000005, so the child process is currently protecting the parent/test session from native crashes rather than preparing for parallelism.
|
|
||
| .jaspToolsSubprocessScript <- function(resultLines, saveLines, | ||
| beforeResultLines = character(0), | ||
| statusExpression = "inherits(result, 'jaspTools.subprocessError')") { |
There was a problem hiding this comment.
This much code as a string seems very prone to breakage and hard to debug?
There was a problem hiding this comment.
Agreed. The long generated child script is gone. The child process now runs normal internal R functions via callr, so the code is easier to debug and covered like ordinary R code.
| if (!is.character(x) || length(x) == 0L) | ||
| return(x) | ||
|
|
||
| tokenPattern <- "(JaspColumn_[[:alnum:]_]+_Encoded|jaspColumn[0-9]+)" |
There was a problem hiding this comment.
Is this still necessary? Decoding is handled by jasp Syntax?
There was a problem hiding this comment.
Agreed. I removed canonicalizeJaspColumnTokens() and the compatibility test around native/legacy encoded tokens. Runtime result decoding is handled by jaspSyntax::decodeAnalysisResults(); expect_equal_tables() now compares strings literally again.
| runAnalysisSubprocessScript <- function() { | ||
| .jaspToolsSubprocessScript( | ||
| beforeResultLines = "warnings <- character(0)", | ||
| resultLines = c( |
There was a problem hiding this comment.
Same stuff about very long code as text
There was a problem hiding this comment.
Agreed. The long code-as-text path in runAnalysis() is gone as part of the callr refactor.
|
Pushed follow-up commit Summary:
Verification:
|
|
Follow-up from the I reproduced the failure through this PR's public path, but the fix belongs in the lower bridge layer and was pushed to the linked
No new Verified locally with |
|
Final cross-repo follow-up from the senior pass: no additional The remaining failures were below the orchestration layer:
With those branches installed together, the reported MixedModels path now completes:
This keeps |
|
I chased the noisy focused-test warnings through the bridge stack. There are no
With those PRs pushed, fresh installed |
Summary
.jaspoption/dataset/QML bridge code and delegate native semantics tojaspSyntaxanalysisOptions()into saved, QML-bound runnable options andanalysisRuntimeOptions()for backend/runtime inspectionrunAnalysis()throughjaspBase::runWrappedAnalysis()with explicit source module/QML provenance and reject already-prepared runtime optionsjaspSyntax::decodeAnalysisResults()and delegate result-state figure decoding to publicjaspBase::decodeJaspResultState()Why
jaspToolsshould not own Desktop/QML/native option semantics. This PR turns it back into the orchestration layer: it asksjaspSyntaxto prepare Desktop-faithful options and asksjaspBaseto replay analyses with source-module context. That removes the parallel parser/encoder drift that made local replay fragile.The last local semantic leak was state-figure decoding:
jaspToolswalkedstate$figuresand calledjaspBase:::decodeplot()directly. That has now moved behindjaspBase::decodeJaspResultState(), leavingjaspToolsresponsible only for reading the state payload and passing it to the owning package.Related PRs
.jasp, QML, dataset, and result-decoding bridge APIs used here.runWrappedAnalysis()replay, standalone state callbacks, R-facing result object decoding, anddecodeJaspResultState().Verification
Rscript -e "pkgload::load_all('C:/JASP-Packages/jaspTools', quiet = TRUE); testthat::test_dir('tests/testthat', reporter = 'summary')"Rscript -e "pkgload::load_all('C:/JASP-Packages/jasp-desktop/Engine/jaspBase', quiet = TRUE); testthat::test_dir('tests/testthat', reporter = 'summary')"git diff --check