Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
01c6450
mvp for testing framework
akselthomsen Sep 15, 2025
6b6b2e4
fix: use callr so covr environments are not overwriting each other
akselthomsen Sep 15, 2025
ffd6860
fix: add callr to wordlist
akselthomsen Sep 15, 2025
39f6f7b
Merge branch 'main' into feat/component-unit-testing
Oct 2, 2025
89d73e1
init vignette
akselthomsen Oct 17, 2025
d5277c9
wip
akselthomsen Oct 20, 2025
c3728f9
fix: covr hack with defer
akselthomsen Nov 6, 2025
6a13d00
feat: working tests in covr
akselthomsen Nov 11, 2025
5a507ae
improve error message
akselthomsen Nov 11, 2025
75a3f63
wip
akselthomsen Dec 2, 2025
9595316
Merge branch 'main' into feat/component-unit-testing
akselthomsen Feb 3, 2026
04ae750
use zephyr
akselthomsen Feb 4, 2026
38543ae
wip
akselthomsen Feb 4, 2026
b88c166
cleanup
akselthomsen Feb 4, 2026
657f7fd
fix validation
akselthomsen Feb 4, 2026
6fff90b
wip: working use of callr::r_session
akselthomsen Feb 4, 2026
03cb002
working test component
akselthomsen Feb 5, 2026
9027ff0
fix returns
akselthomsen Feb 5, 2026
23edf33
check implementation
akselthomsen Feb 6, 2026
e67e8ae
updated all tests
akselthomsen Feb 6, 2026
09c2561
fix pkgdown
akselthomsen Feb 6, 2026
4313311
remove expect_disjoint for older testthat versions
akselthomsen Feb 6, 2026
afbb74f
fix: remove comments
akselthomsen Feb 6, 2026
3a94157
remove mroe comments
akselthomsen Feb 6, 2026
af26c71
Increment version number to 0.0.0.9017
akselthomsen Feb 6, 2026
f64ffb5
feat: update docs
akselthomsen Feb 6, 2026
0df59e4
test coverage before eval
akselthomsen Feb 6, 2026
068ec68
replace .self
akselthomsen Feb 6, 2026
92a642a
fix: use domain
akselthomsen Feb 6, 2026
f3b2652
print method and extra testing
akselthomsen Feb 6, 2026
eb6b38d
fix ci
akselthomsen Feb 6, 2026
0f453f9
fix remove standard
akselthomsen Feb 6, 2026
f25c8f9
fix formatting
akselthomsen Feb 6, 2026
6ca000e
fix wording
akselthomsen Feb 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 8 additions & 5 deletions DESCRIPTION
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
Package: mighty.component
Title: Standard Components for ADaM Generation
Version: 0.0.0.9016
Title: Mighty Components for ADaM Generation
Version: 0.0.0.9017
Authors@R: c(
person("Aksel", "Thomsen", , "oath@enovonordisk.com", role = c("aut", "cre")),
person("Matthew", "Phelps", , "mewp@enovonordisk.com", role = c("aut")),
person("Novo Nordisk A/S", role = "cph")
)
Description: Standard components library to be used inside the 'mighty'
Description: Components to be used inside the 'mighty'
framework for generation of ADaM programs.
License: Apache License (>= 2)
URL: https://novonordisk-opensource.github.io/mighty.component,
Expand All @@ -16,13 +16,16 @@ Depends:
Imports:
cli,
glue,
R6,
R6 (>= 2.4.0),
rlang,
whisker,
xmlparsedata,
xml2
xml2,
zephyr
Suggests:
admiral,
callr,
covr,
dplyr,
jsonlite,
knitr,
Expand Down
2 changes: 2 additions & 0 deletions NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ export(get_component)
export(get_rendered_component)
export(get_rendered_standard)
export(get_standard)
export(get_test_component)
export(list_standards)
export(mighty_component)
export(mighty_component_rendered)
export(mighty_component_test)
importFrom(R6,R6Class)
importFrom(rlang,list2)
importFrom(whisker,whisker.render)
44 changes: 44 additions & 0 deletions R/component.R
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,47 @@ get_rendered_component <- function(component, params = list()) {
x <- get_component(component)
do.call(what = x$render, args = params)
}

#' Create a testable component for unit testing
#'
#' @description
#' Creates a `mighty_component_test` object from a rendered component,
#' enabling structured unit testing with optional coverage checking.
#'
#' See [mighty_component_test] for a description of the testing workflow.
#'
#' @inheritParams get_component
#' @param check_coverage `logical(1)` Whether to automatically check test
#' coverage when the test completes. If `TRUE` (default), coverage is
#' verified via `test_component$check_coverage()` in a deferred call.
#' @param teardown_env The environment in which to register the deferred
#' coverage check. Defaults to the caller's environment (`parent.frame()`).
#' This controls when `check_coverage()` executes during test teardown.
#'
#' @return A `mighty_component_test` object.
#'
#' @seealso [get_rendered_component()], [mighty_component_test]
#'
#' @export
get_test_component <- function(
component,
params = list(),
check_coverage = TRUE,
teardown_env = parent.frame()
) {
x <- get_rendered_component(component, params)

test_component <- mighty_component_test$new(
template = x$template,
id = x$id
)

if (check_coverage) {
withr::defer(
expr = test_component$check_coverage(),
envir = teardown_env
)
}

test_component
}
19 changes: 19 additions & 0 deletions R/mighty.component-options.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#' @title Options for mighty.component
#' @name mighty.component-options
#' @description
#' `r zephyr::list_options(as = "markdown", .envir = "mighty.component")`
NULL

#' @title Internal parameters for reuse in functions
#' @name mighty.component-options-params
#' @eval zephyr::list_options(as = "params", .envir = "mighty.component")
#' @details
#' See [mighty.component-options] for more information.
#' @keywords internal
NULL

zephyr::create_option(
name = "verbosity_level",
default = NA_character_,
desc = "Verbosity level for functions in mighty.component. See [zephyr::verbosity_level] for details." # nolint: line_length_linter
)
24 changes: 12 additions & 12 deletions R/mighty_component.R
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
#' Mighty standard component
#' Mighty component
#' @description
#' Class for a generic mighty standard component.
#' Class for a generic mighty component.
#'
#' In the mighty framework, a "component" is a code template that processes
#' input data and returns a modified version with new columns or rows.
#' Standard components share a common structure and roxygen-like documentation pattern,
#' Mighty components share a common structure and roxygen-like documentation pattern,
#' facilitating their use inside mighty.
#'
#' @details
Expand All @@ -23,24 +23,24 @@
#' | `@description` | Description of the component | `@description text text` |
#' | `@param` | Specifies input used to render the component | `@param variable new var`|
#' | `@type` | Specifies type: `r mighty.component:::valid_types()` | `@type derivation` |
#' | `@depends` | Required input variable (repeat if several) | `@depends .self USUBJID` |
#' | `@depends` | Required input variable (repeat if several) | `@depends {{ domain }} USUBJID` |
#' | `@outputs` | Variables created (repeat if several) | `@outputs NEWVAR` |
#' | `@code` | Everything under this tag defines the component code | `@code` |
#'
#' ### Conventions
#'
#' A template for a standard components follow these conventions:
#'
#' 1. The input data set is always called `.self`.
#' 1. The input data set is always called `{{ domain }}`.
#' 1. Additional parameters used to render the template into R code are documented with the `@param` tag.
#' 1. The template ends with creating a modified version of `.self`.
#' 1. The template ends with creating a modified version of `{{ domain }}`.
#' 1. Template documented with the roxygen-like tags above
#'
#' ### Example
#'
#' Below is an example of a mighty component template that
#' creates a new dynamic variable `variable` as twice the value
#' of the dynamic input `x`, that should already by in the input data set `.self`.
#' of the dynamic input `x`, that should already by in the input data set `{{ domain }}`.
#'
#' ```r
#' #' @title Title for my component
Expand All @@ -50,10 +50,10 @@
#' #' @param variable dynamic output if applicable
#' #' @param x some other input to the component
#' #' @type derivation
#' #' @depends .self {{ x }}
#' #' @depends {{ domain }} {{ x }}
#' #' @outputs {{ variable }}
#' #' @code
#' .self <- .self |>
#' {{ domain }} <- {{ domain }} |>
#' dplyr::mutate(
#' {{ variable }} = 2 * {{ x }}
#' )
Expand All @@ -63,13 +63,13 @@
#' the rendered code used in mighty becomes:
#'
#' ```r
#' .self <- .self |>
#' {{ domain }} <- {{ domain }} |>
#' dplyr::mutate(
#' A = 2 * B
#' )
#' ```
#'
#' @seealso [get_standard()], [mighty_component_rendered]
#' @seealso [get_standard()], [get_component()], [mighty_component_rendered]
#' @export
mighty_component <- R6::R6Class(
classname = "mighty_component",
Expand Down Expand Up @@ -104,7 +104,7 @@ mighty_component <- R6::R6Class(
}
),
active = list(
#' @field id Component ID
#' @field id Component ID.
id = \() private$.id,
#' @field title Title for the component.
title = \() private$.title,
Expand Down
75 changes: 30 additions & 45 deletions R/mighty_component_rendered.R
Original file line number Diff line number Diff line change
@@ -1,33 +1,24 @@
#' Rendered mighty standard component
#' Rendered mighty component
#' @description
#' Class for a rendered mighty standard component.
#' Class for a rendered mighty component.
#'
#' Once rendered a component can be used to:
#'
#' * Stream into an R script
#' * Evaluate the generated code in an environment
#' * Test code against expected output
#' * Calculate test coverage
#'
#' @seealso [get_rendered_standard()]
#' @seealso [get_rendered_standard()], [get_rendered_component()]
#' @export
mighty_component_rendered <- R6::R6Class(
classname = "mighty_component_rendered",
inherit = mighty_component,
public = list(
#' @description
#' Create standard component from rendered template.
#' Create component from rendered template.
#' @param template `character` Rendered template such as output from `mighty_component$render()`.
#' @param id `character` ID of the component. Either name of standard or path to local.
initialize = function(template, id) {
super$initialize(template, id)

validate_component_code(self$code)

private$.params <- data.frame(
name = character(),
description = character()
)
msr_initialize(template, id, self, private, super)
},
#' @description
#' Print rendered component
Expand All @@ -46,29 +37,23 @@ mighty_component_rendered <- R6::R6Class(
#' @param envir Environment to evaluate in. Parsed to `eval()`.
#' Defaults to using the current environment with `parent.frame()`.
eval = function(envir = parent.frame()) {
eval(
expr = parse(text = self$code),
envir = envir
)
},
#' @description
#' Test component against expected output.
#' @param expected The expected output in `value` after evaluation
#' @param value Name of the object used to compare against after evaluating
#' the component. Defaults to `"domain"`.
#' @param envir Parent environment to use for evaluation of test code.
#' Defaults to using the current environment with `parent.frame()`.
test = function(expected, value = "domain", envir = parent.frame()) {
msr_test(expected, value, envir, self, private)
},
#' @description
#' Calculate test coverage for already run tests
test_coverage = function() {
msr_test_coverage(self)
msr_eval(envir, self)
}
)
)

#' @noRd
msr_initialize <- function(template, id, self, private, super) {
super$initialize(template, id)

validate_component_code(self$code)

private$.params <- data.frame(
name = character(),
description = character()
)
}

#' @noRd
msr_print <- function(self, super) {
cli::cli({
Expand All @@ -89,18 +74,18 @@ msr_stream <- function(path, self) {
}

#' @noRd
msr_test <- function(expected, value, envir, self, private) {
env <- new.env(parent = envir)
self$eval(envir = env)
testthat::expect_equal(
object = env[[value]],
expected = expected,
ignore_attr = TRUE
msr_eval <- function(envir, self) {
zephyr::msg_verbose(
Comment thread
matthew-phelps marked this conversation as resolved.
message = c(
">" = "Evaluating component {.field {self$title}}",
"i" = "{.emph Code:}",
"{.code {self$code}}"
),
msg_fun = cli::cli_bullets
)
return(invisible(self))
}

#' @noRd
msr_test_coverage <- function(self) {
return(numeric(1))
eval(
expr = parse(text = self$code),
envir = envir
)
}
Loading