mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-15 04:18:05 +00:00
test(r): native pipe chain fixtures and resolution tests
This commit is contained in:
parent
31e006fb5e
commit
e9be500739
11 changed files with 328 additions and 0 deletions
3
gitnexus/test/fixtures/lang-resolution/r-native-pipes/pkgmain/DESCRIPTION
vendored
Normal file
3
gitnexus/test/fixtures/lang-resolution/r-native-pipes/pkgmain/DESCRIPTION
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
Package: pkgmain
|
||||
Title: Native Pipe Fixture Main
|
||||
Version: 0.1.0
|
||||
1
gitnexus/test/fixtures/lang-resolution/r-native-pipes/pkgmain/NAMESPACE
vendored
Normal file
1
gitnexus/test/fixtures/lang-resolution/r-native-pipes/pkgmain/NAMESPACE
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
export(stage_one)
|
||||
54
gitnexus/test/fixtures/lang-resolution/r-native-pipes/pkgmain/R/a.R
vendored
Normal file
54
gitnexus/test/fixtures/lang-resolution/r-native-pipes/pkgmain/R/a.R
vendored
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
stage_one <- function(d) d
|
||||
stage_two <- function(d, k) d
|
||||
stage_three <- function(d, data) d
|
||||
uniq_helper <- function(d) d
|
||||
amb_stage <- function(d) d
|
||||
|
||||
# Module-level pipe: sourced from the File node.
|
||||
piped_top <- c(1, 2) |> stage_one() |> stage_two(3)
|
||||
|
||||
# Placeholder argument: `_` is not a call and produces no edge.
|
||||
placeholder_user <- function(df) {
|
||||
df |> stage_three(data = _)
|
||||
}
|
||||
|
||||
# Lambda stage: the call inside the lambda belongs to the enclosing function.
|
||||
lambda_user <- function(df) {
|
||||
df |> (\(d) uniq_helper(d))()
|
||||
}
|
||||
|
||||
# Namespaced stage, unique name in another package (qualifier is unused).
|
||||
ns_user <- function(df) {
|
||||
df |> pkgother::ext_fn() |> stage_one()
|
||||
}
|
||||
|
||||
# Namespaced stage whose name is also defined in THIS file (fork #7).
|
||||
ns_amb_user <- function(df) {
|
||||
df |> pkgother::amb_stage()
|
||||
}
|
||||
|
||||
# Multi-line chain, leading `|>` on continuation lines, builtin `paste` stage.
|
||||
multiline_user <- function(df) {
|
||||
df |>
|
||||
stage_one() |>
|
||||
stage_two(2) |>
|
||||
paste("x") |>
|
||||
stage_three(data = _)
|
||||
}
|
||||
|
||||
# R6 method chain ending in a self$ member stage.
|
||||
Cls <- R6::R6Class("Cls", public = list(
|
||||
run = function(df) {
|
||||
df |> stage_one() |> self$post()
|
||||
},
|
||||
post = function(d) d
|
||||
))
|
||||
|
||||
plain_user <- function(df) {
|
||||
uniq_helper(df)
|
||||
}
|
||||
|
||||
# Member stage on an object of unknown type.
|
||||
member_user <- function(obj, df) {
|
||||
df |> obj$method(1) |> stage_one()
|
||||
}
|
||||
19
gitnexus/test/fixtures/lang-resolution/r-native-pipes/pkgmain/R/b.R
vendored
Normal file
19
gitnexus/test/fixtures/lang-resolution/r-native-pipes/pkgmain/R/b.R
vendored
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
Type <- R6::R6Class("Type", public = list(
|
||||
score = function(d) d
|
||||
))
|
||||
|
||||
# Member stage on a typed local (constructed via Type$new()).
|
||||
typed_user <- function(df) {
|
||||
obj <- Type$new()
|
||||
df |> obj$score() |> stage_one()
|
||||
}
|
||||
|
||||
# Backticked stage with no definition anywhere.
|
||||
bt_user <- function(df) {
|
||||
df |> `my fn`() |> stage_two(1)
|
||||
}
|
||||
|
||||
# Same name defined in both other packages (fork #7).
|
||||
ns_amb2_user <- function(df) {
|
||||
df |> pkgother::amb_only() |> pkgthird::amb_only()
|
||||
}
|
||||
5
gitnexus/test/fixtures/lang-resolution/r-native-pipes/pkgmain/R/decoy_first.R
vendored
Normal file
5
gitnexus/test/fixtures/lang-resolution/r-native-pipes/pkgmain/R/decoy_first.R
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
# First function of the package in load order: a decoy that must own only its
|
||||
# own pipe edge, never the edges of pipes written in later functions/files.
|
||||
decoy_first <- function(d) {
|
||||
d |> uniq_helper()
|
||||
}
|
||||
9
gitnexus/test/fixtures/lang-resolution/r-native-pipes/pkgmain/R/nested.R
vendored
Normal file
9
gitnexus/test/fixtures/lang-resolution/r-native-pipes/pkgmain/R/nested.R
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
# Nested function sharing a name with a later top-level function (plan O1).
|
||||
nested_outer <- function(d) {
|
||||
nest_fn <- function(x) x
|
||||
d |> nest_fn() |> stage_two(1)
|
||||
}
|
||||
nest_fn <- function(x) x
|
||||
nested_caller <- function(d) {
|
||||
d |> nest_fn()
|
||||
}
|
||||
3
gitnexus/test/fixtures/lang-resolution/r-native-pipes/pkgother/DESCRIPTION
vendored
Normal file
3
gitnexus/test/fixtures/lang-resolution/r-native-pipes/pkgother/DESCRIPTION
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
Package: pkgother
|
||||
Title: Native Pipe Fixture Other
|
||||
Version: 0.1.0
|
||||
3
gitnexus/test/fixtures/lang-resolution/r-native-pipes/pkgother/R/e.R
vendored
Normal file
3
gitnexus/test/fixtures/lang-resolution/r-native-pipes/pkgother/R/e.R
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
ext_fn <- function(d) d
|
||||
amb_stage <- function(d) d
|
||||
amb_only <- function(d) d
|
||||
3
gitnexus/test/fixtures/lang-resolution/r-native-pipes/pkgthird/DESCRIPTION
vendored
Normal file
3
gitnexus/test/fixtures/lang-resolution/r-native-pipes/pkgthird/DESCRIPTION
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
Package: pkgthird
|
||||
Title: Native Pipe Fixture Third
|
||||
Version: 0.1.0
|
||||
1
gitnexus/test/fixtures/lang-resolution/r-native-pipes/pkgthird/R/t.R
vendored
Normal file
1
gitnexus/test/fixtures/lang-resolution/r-native-pipes/pkgthird/R/t.R
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
amb_only <- function(d) d
|
||||
|
|
@ -671,3 +671,230 @@ describe('R caller attribution', () => {
|
|||
}
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* R native pipe (`|>`) chains: baseline characterisation at the landed
|
||||
* caller-attribution fix. Every assertion here passes without any pipe-specific
|
||||
* production support; the tests lock the current behaviour before NAMESPACE /
|
||||
* importFrom resolution work touches the resolver.
|
||||
* The fixture is synthetic (`r-native-pipes/`, three packages); one function per
|
||||
* scenario so each assertion is an exact `source -> targets` set.
|
||||
* magrittr `%>%` is deliberately NOT covered (no support claimed either way).
|
||||
* Two tests pin known-wrong name-only results for `pkg::fn` stages (fork #7).
|
||||
*/
|
||||
describe('R native pipe chains', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(path.join(FIXTURES, 'r-native-pipes'), () => {});
|
||||
}, 60000);
|
||||
|
||||
const callEdges = (sourceName: string, fileSuffix: string) =>
|
||||
getRelationships(result, 'CALLS').filter(
|
||||
(e) => e.source === sourceName && e.sourceFilePath.endsWith(fileSuffix),
|
||||
);
|
||||
|
||||
/** Sorted unique CALLS targets of the source named `sourceName` in the file ending `fileSuffix`. */
|
||||
const callsFrom = (sourceName: string, fileSuffix: string): string[] =>
|
||||
[...new Set(callEdges(sourceName, fileSuffix).map((e) => e.target))].sort();
|
||||
|
||||
/** Sorted `Label:name` of every CALLS source in the file ending `fileSuffix` that targets `target`. */
|
||||
const sourceKeys = (fileSuffix: string, target: string): string[] =>
|
||||
getRelationships(result, 'CALLS')
|
||||
.filter((e) => e.sourceFilePath.endsWith(fileSuffix) && e.target === target)
|
||||
.map((e) => `${e.sourceLabel}:${e.source}`)
|
||||
.sort();
|
||||
|
||||
const A = 'pkgmain/R/a.R';
|
||||
const B = 'pkgmain/R/b.R';
|
||||
|
||||
// ─── module-level chain ─────────────────────────────────────────────────
|
||||
describe('module-level chain', () => {
|
||||
it('sources every stage of a top-level chain from the File node', () => {
|
||||
expect(callsFrom('a.R', A)).toEqual(['stage_one', 'stage_two']);
|
||||
});
|
||||
|
||||
it('labels the top-level chain source as File for stage_one', () => {
|
||||
expect(sourceKeys(A, 'stage_one').filter((k) => k.startsWith('File:'))).toEqual(['File:a.R']);
|
||||
});
|
||||
|
||||
it('labels the top-level chain source as File for stage_two', () => {
|
||||
expect(sourceKeys(A, 'stage_two').filter((k) => k.startsWith('File:'))).toEqual(['File:a.R']);
|
||||
});
|
||||
|
||||
it('resolves the top-level stages as local calls at 0.85', () => {
|
||||
expect(
|
||||
callEdges('a.R', A)
|
||||
.map((e) => `${e.target}:${e.rel.reason}:${e.rel.confidence}`)
|
||||
.sort(),
|
||||
).toEqual(['stage_one:local-call:0.85', 'stage_two:local-call:0.85']);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── multi-line chain ───────────────────────────────────────────────────
|
||||
describe('multi-line chain with a builtin stage', () => {
|
||||
it('resolves every user-defined stage of a five-stage chain', () => {
|
||||
expect(callsFrom('multiline_user', A)).toEqual(['stage_one', 'stage_three', 'stage_two']);
|
||||
});
|
||||
|
||||
it('emits no edge for the builtin-named paste stage', () => {
|
||||
expect(callsFrom('multiline_user', A).includes('paste')).toBe(false);
|
||||
});
|
||||
|
||||
it('emits no CALLS edge to a paste target anywhere', () => {
|
||||
expect(getRelationships(result, 'CALLS').filter((e) => e.target === 'paste')).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── placeholder ────────────────────────────────────────────────────────
|
||||
describe('placeholder argument', () => {
|
||||
it('resolves only the stage that takes the placeholder', () => {
|
||||
expect(callsFrom('placeholder_user', A)).toEqual(['stage_three']);
|
||||
});
|
||||
|
||||
it('emits no CALLS edge to the placeholder `_`', () => {
|
||||
expect(getRelationships(result, 'CALLS').filter((e) => e.target === '_')).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── lambda stage ───────────────────────────────────────────────────────
|
||||
describe('lambda stage', () => {
|
||||
it('sources the lambda body call from the enclosing function', () => {
|
||||
expect(callsFrom('lambda_user', A)).toEqual(['uniq_helper']);
|
||||
});
|
||||
|
||||
it('resolves the lambda body call as a 0.85 local call', () => {
|
||||
expect(callEdges('lambda_user', A).map((e) => `${e.rel.reason}:${e.rel.confidence}`)).toEqual(
|
||||
['local-call:0.85'],
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── attribution across functions ───────────────────────────────────────
|
||||
describe('two pipe functions in one package', () => {
|
||||
it('gives the decoy first function only its own pipe edge', () => {
|
||||
expect(callsFrom('decoy_first', 'pkgmain/R/decoy_first.R')).toEqual(['uniq_helper']);
|
||||
});
|
||||
|
||||
it('does not source later pipes from the decoy first function', () => {
|
||||
expect(
|
||||
sourceKeys('pkgmain/R/decoy_first.R', 'stage_one').concat(
|
||||
sourceKeys('pkgmain/R/decoy_first.R', 'stage_three'),
|
||||
),
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
it('keeps a plain call and a lambda-stage call in separate sources', () => {
|
||||
expect(sourceKeys(A, 'uniq_helper')).toEqual(['Function:lambda_user', 'Function:plain_user']);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── R6 method chain ────────────────────────────────────────────────────
|
||||
describe('R6 method chain', () => {
|
||||
it('sources the chain from the Method and calls exactly the stage and self$post', () => {
|
||||
expect(callsFrom('run', A)).toEqual(['post', 'stage_one']);
|
||||
});
|
||||
|
||||
it('targets a Method for the self$post stage', () => {
|
||||
expect(callEdges('run', A).find((e) => e.target === 'post')?.targetLabel).toBe('Method');
|
||||
});
|
||||
|
||||
it('targets a Function for the stage_one stage', () => {
|
||||
expect(callEdges('run', A).find((e) => e.target === 'stage_one')?.targetLabel).toBe(
|
||||
'Function',
|
||||
);
|
||||
});
|
||||
|
||||
it('labels the chain source as Method:run', () => {
|
||||
expect(sourceKeys(A, 'post')).toEqual(['Method:run']);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── member stages ──────────────────────────────────────────────────────
|
||||
describe('member stage on a typed object', () => {
|
||||
it('resolves obj$score() and the following stage', () => {
|
||||
expect(callsFrom('typed_user', B)).toEqual(['score', 'stage_one']);
|
||||
});
|
||||
|
||||
it('targets a Method for the typed member stage', () => {
|
||||
expect(callEdges('typed_user', B).find((e) => e.target === 'score')?.targetLabel).toBe(
|
||||
'Method',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('member stage on an untyped object', () => {
|
||||
it('emits no edge for the untyped member stage and resolves the next stage', () => {
|
||||
expect(callsFrom('member_user', A)).toEqual(['stage_one']);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── namespaced stages ──────────────────────────────────────────────────
|
||||
describe('namespaced stage with a unique name in another package', () => {
|
||||
it('resolves pkgother::ext_fn() and the following stage', () => {
|
||||
expect(callsFrom('ns_user', A)).toEqual(['ext_fn', 'stage_one']);
|
||||
});
|
||||
|
||||
it('resolves the namespaced stage into the other package', () => {
|
||||
expect(callEdges('ns_user', A).find((e) => e.target === 'ext_fn')?.targetFilePath).toBe(
|
||||
'pkgother/R/e.R',
|
||||
);
|
||||
});
|
||||
|
||||
it('resolves it by the 0.5 global-name-fallback (the qualifier is unused)', () => {
|
||||
expect(
|
||||
callEdges('ns_user', A)
|
||||
.filter((e) => e.target === 'ext_fn')
|
||||
.map((e) => `${e.rel.reason}:${e.rel.confidence}`),
|
||||
).toEqual(['global-name-fallback:0.5']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('namespaced stage whose name is also defined in the calling file', () => {
|
||||
it('documents current name-only behaviour (fork #7)', () => {
|
||||
// `pkgother::amb_stage()` is qualified, yet the edge goes to the LOCAL definition
|
||||
// (0.85 local-call): the qualifier is discarded. A false edge; fork #7 should flip this.
|
||||
expect(
|
||||
callEdges('ns_amb_user', A).map((e) => `${e.target}:${e.targetFilePath}:${e.rel.reason}`),
|
||||
).toEqual(['amb_stage:pkgmain/R/a.R:local-call']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('namespaced stages whose name is defined in two other packages', () => {
|
||||
it('documents current name-only behaviour (fork #7)', () => {
|
||||
// `pkgother::amb_only() |> pkgthird::amb_only()`: ambiguous by name alone, so no edge
|
||||
// even though each qualifier names exactly one definition. Fork #7 should flip this.
|
||||
expect(callsFrom('ns_amb2_user', B)).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── backticked stage ───────────────────────────────────────────────────
|
||||
describe('backticked stage with no definition', () => {
|
||||
it('emits no edge for it and still resolves the following stage', () => {
|
||||
expect(callsFrom('bt_user', B)).toEqual(['stage_two']);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── O1: nested function sharing a name with a later top-level function ─
|
||||
describe('nested function sharing a name with a top-level function (plan O1)', () => {
|
||||
const N = 'pkgmain/R/nested.R';
|
||||
const targetStartLine = (fn: string): number | undefined => {
|
||||
const edge = callEdges(fn, N).find((e) => e.target === 'nest_fn');
|
||||
return edge ? result.graph.getNode(edge.rel.targetId)?.properties.startLine : undefined;
|
||||
};
|
||||
|
||||
it('resolves a pipe stage to the same-named nested function from the enclosing function', () => {
|
||||
expect(callsFrom('nested_outer', N)).toEqual(['nest_fn', 'stage_two']);
|
||||
});
|
||||
|
||||
it('resolves the same name from another function to the top-level definition', () => {
|
||||
expect(callsFrom('nested_caller', N)).toEqual(['nest_fn']);
|
||||
});
|
||||
|
||||
it('binds the enclosing function to the nested definition, not the top-level one', () => {
|
||||
expect(targetStartLine('nested_outer') ?? Infinity).toBeLessThan(
|
||||
targetStartLine('nested_caller') ?? -Infinity,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue