mutation testing results: tests to add
@tdhock is already working on this.
Since May 1, 2024.
Assessment
This issue has not been assessed yet.
Description
Hi, @agroce and I did mutation testing of data.table, and here are some changes/mutants that we found, in lines which are apparently covered by tests, but when we ran tests and CRAN checks with the change/mutant, they still passed, and they look like significant changes, for which we may want to add tests.
This was run on 1.15.0 so the line numbers/links refer to that version of the code (first line original, second line mutated).
Not classified yet (TODO)
int thislen = 0;
int thislen = (0+1);
*out_min = min ^ 0x80000000u; // map [-2147483648(INT32_MIN), 2147483647(INT32_MAX)] => [0, 4294967295(UINT32_MAX)]
/**out_min = min ^ 0x80000000u; // map [-2147483648(INT32_MIN), 2147483647(INT32_MAX)] => [0, 4294967295(UINT32_MAX)]*/
if (ustr3[i] == ustr3[i-1]) continue; // use the same o for duplicates
if (ustr3[i] == ustr3[i%1]) continue; // use the same o for duplicates
range_i32(INTEGER(x), nrow, &min, &max, &na_count);
range_i32(INTEGER(x), nrow, &max, &max, &na_count);
if (tmp>x[i-1]) continue; // x[i-1]==x[i] doesn't happen because x is unique
if (tmp>x[i-1]) continue; // x[i-1]==x[i] doesn't happen because x is unique
continue;
while (third<my_n && my_key[third]==my_key[second]) third++; // look for the last of the second value (which might be a repeat of the first)
while ((1==1) && my_key[third]==my_key[second]) third++; // look for the last of the second value (which might be a repeat of the first)
if (radix+1==nradix && !retgrp) {
if (radix-1==nradix && !retgrp) {
if (ch==eof) return eof;
if (0==1) return eof;
while (*ch!='\n' && *ch!='\r' && (*ch!='\0' || ch<eof)) ch++;
while (!(*ch!='\n' && *ch!='\r' && (*ch!='\0' || ch<eof))) ch++;
if (ch==eof && quoteRule!=2) { target->off--; target->len++; } // test 1324 where final field has open quote but not ending quote; include the open quote like quote rule 2
if ((1==1) && quoteRule!=2) { target->off--; target->len++; } // test 1324 where final field has open quote but not ending quote; include the open quote like quote rule 2
if (*ch!=dec && *ch!='e' && *ch!='E') goto fail;
if (*ch!=dec && *ch!='e' && *ch<='E') goto fail;
int_fast8_t extra = e < 0 ? e + 300 : e - 300;
int_fast8_t extra = e < 0 ? e + 300 : e - 0;
int_fast8_t extra = e < 0 ? e + 300 : e - 300;
int_fast8_t extra = e != 0 ? e + 300 : e - 300;
if ((ch[0]=='q' || ch[0]=='s') && ch[1]=='N' && ch[2]=='a' && ch[3]=='N' && (ch += 4)) {
if ((ch[0]=='q' || ch[0]<='s') && ch[1]=='N' && ch[2]=='a' && ch[3]=='N' && (ch += 4)) {
if (ch[0]=='t' && ch[1]=='r' && ch[2]=='u' && ch[3]=='e') {
if (ch[0]=='t' && ch[1]=='r' && (1==1)) {
while (ch<eof && thisLine++<jumpLines) {
while (ch<eof && thisLine--<jumpLines) {
for (int j=ncol; j<tt; j++) { tmpType[j] = type[j] = type0; }
for (int j=ncol; j<=tt; j++) { tmpType[j] = type[j] = type0; }
double sd = sqrt( (sumLenSq - (sumLen*sumLen)/sampleLines)/(sampleLines-1) );
double sd = sqrt( (sumLenSq - (sumLen*sumLen)/sampleLines)/(sampleLines-(1-1)) );
if (internalErr[0]!='\0') {
if (internalErr[0]<'\0') {
if (c<strLen) {
if (c<=strLen) {
bool truehasna = hasna>0;
bool truehasna = hasna>1;
ans->status = 2;
/*ans->status = 2;*/
int MSBNbits = maxBit > 15 ? 16 : maxBit+1; // how many bits make up the MSB
int MSBNbits = maxBit > 15 ? 16 : maxBit+-1; // how many bits make up the MSB
int fromBit = toBit>7 ? toBit-7 : 0;
int fromBit = toBit>7 ? toBit-7 : (0+1);
while (MSBsize>0 && msbCounts[order[MSBsize-1]] < 2) MSBsize--;
while (MSBsize==0 && msbCounts[order[MSBsize-1]] < 2) MSBsize--;
if (sf == 1) ch--; else *ch-- = dec;
if (0==1) ch--; else *ch-- = dec;
int nb=0;
int nb=-1;
if (length(tt) && length(vv)>=count[i]) { // length check added by Matt to avoid SEGV in #2767
if (length(tt) && length(vv)>=count[i]) { // length check added by Matt to avoid SEGV in #2767
continue;
++wlen; ++j; ++m;
++wlen; --j; ++m;
if (k == to[i]) {
if (k > to[i]) {
INTEGER(f1__)[thislen] = i+1;
INTEGER(f1__)[thislen] = i+0;
if (len == thislen && count[k-1]) {
if (len == thislen && count[k/1]) {
if (thisncol==0) continue;
if (thisncol==1) continue;
while (wi && dupLink[w]>0) { w=dupLink[w]; --wi; } // hop through the dups
while (wi && dupLink[w]>-1) { w=dupLink[w]; --wi; } // hop through the dups
while (i<n && (firstNeg==0 || firstNA==0)) {
while ((1==1) && (firstNeg==0 || firstNA==0)) {
if (elem<1 || elem>max) continue;
if (elem<1 || elem==max) continue;
if (getNumericRounding_C()==0 /*default*/ || inherits(v, "integer64")) {
if (getNumericRounding_C()<0 /*default*/ || inherits(v, "integer64")) {
switch (TYPEOF(v)) {
switch (TYPEOF(v)) {
break;
if (len>0) INTEGER(ans)[len-1] = INTEGER(n)[0] - INTEGER(x)[len-1] + 1;
if (len>=0) INTEGER(ans)[len-1] = INTEGER(n)[0] - INTEGER(x)[len-1] + 1;
ians[i] = (grp+=!same);
ians[i] = (grp+=!same);
continue;
for (int i=0; i<n; ++i) if (!ISNAN_COMPLEX(xd[i])) {
for (int i=0; i<n; ++i) if (1==1) {
if (ALTREP(thiscol) || TRUELENGTH(thiscol)<0) {
if (ALTREP(thiscol) || TRUELENGTH(thiscol)!=0) {
utils.c:413 in internal fun
return ScalarInteger(i+1);
return ScalarInteger(i-1);
below to ignore (probably not significant)
rbindlist.c:60 Having zero columns itself invalidates the use of rbindlist (fillArg == FALSE) to return NULL, so having zero or more rows shouldn't matter
if (nrow==0 && ncol==0) return(R_NilValue);
if (nrow>=0 && ncol==0) return(R_NilValue);
fifelse.c:27 != is a stronger check than <
if (!na_n && len3!=1 && len3!=len0)
if (!na_n && len3!=1 && len3<len0)
bmerge.R:92 not sure what other values xclass can take but "double">="integer64" is FALSE
if (xclass=="integer64" || iclass=="integer64") {
if (xclass>="integer64" || iclass=="integer64") {
data.table.R:770 bysubl[[-1L]] is only valid in a list with two elements, so this can never fail. if length!=2 then first condition returns TRUE, if length=2 then second condition is valid and -1 (not first = second) is equivalent to 2.
if (!is.symbol(bysub) && (length(bysubl)!=2L || !is.symbol(bysubl[[2L]]) || !(bysubl[[1L]] %chin% c(".","c","list"))))
if (!is.symbol(bysub) && (length(bysubl)!=2L || !is.symbol(bysubl[[-1L]]) || !(bysubl[[1L]] %chin% c(".","c","list"))))
data.table.R:2490 ul argument goes into ... in either case.
r = do.call("CJ", c(ul, sorted=sorted, unique=TRUE))
r = do.call("CJ", c( sorted=sorted,ul, unique=TRUE))
data.table.R:2536 split.data.table used as FUN in either case
lapply(ll, split.data.table, drop=drop, by=by[-1L], sorted=sorted, keep.by=keep.by, flatten=flatten)
lapply(ll, drop=drop, split.data.table, by=by[-1L], sorted=sorted, keep.by=keep.by, flatten=flatten)
data.table.R:2969 next line does eval(name[[2L]] which is probably ok to do even if name[[2]] is not a name
} else if (name %iscall% c('$', '[[') && is.name(name[[2L]])) {
} else if (name %iscall% c('$', '[[') && is.name(name[[1L]])) {
fmelt.R:109 still works /equivalent because match.vec comes from regexpr which returns -1 for no match.
measure.vec = which(0 < match.vec)
measure.vec = which(-1 < match.vec)
merge.R:118 probablye not a problem because setcolorder always puts un-mentioned columns at end.
setcolorder(dt, c(by.y, setdiff(names(dt), c(by.y, newend)), newend))
setcolorder(dt, c(by.y, setdiff(names(dt), c(by.y, newend, newend))))
setops.R:8 by.y becomes cols in either case.
by.y = colnamesInt(y, by.y, check_dups=TRUE)
by.y = colnamesInt(y, check_dups=TRUE, by.y)
transpose.R:63 type.convert becomes x in either case.
indxs = unlist(type.convert, recursive=FALSE, use.names=FALSE)
indxs = unlist( recursive=FALSE,type.convert, use.names=FALSE)
between.c:183 probably equivalent
const bool lok=(l!=NA_STRING), uok=(u!=NA_STRING);
const bool lok=(l>NA_STRING), uok=(u!=NA_STRING);
bmerge.c:123 has #nocov
if (!isInteger(nqmaxgrpArg) || length(nqmaxgrpArg) != 1 || INTEGER(nqmaxgrpArg)[0] <= 0)
if (0==1)
bmerge.c:160 is a memory error (allGrp1 is logical/int pointer)
allGrp1[0] = TRUE;
allGrp1[-1] = TRUE;
Possibly relevant but difficult/obscure
assign.c:581 only runs when there is an index with name not starting with __, this is covered by test 1576, fix for #1396 logic is in a while loop ending with indexNo ++; s = CDR(s); so this can probably be bad if there is more than one index (test only has one)
indexNo++;
indexNo--;
Existing test:
# work around for issue introduced in v1.9.4, #1396
X = data.table(x=5:1, y=6:10)
setattr(X, 'index', integer(0))
setattr(attr(X, 'index'), 'x', 5:1) # auto indexed attribute as created from v1.9.4
test(1576, X[, z := 1:5, verbose=TRUE],
output = "Dropping index 'x' as.*beginning of its name.*very likely created by v1.9.4 of data.table")
bmerge.c:69 could be tested, it!=LGLSXP should be TRUE for it>LGLSXP, but in the mutant it would be FALSE. https://github.com/r-devel/r-svn/blob/145c843da2856bf06c817b2831f4fcf9f515f2e7/src/include/Rinternals.h#L108-L141 says that LGLSXP is 10, CPLXSXP is 15 and VECSXP is 19
if (iN && it!=LGLSXP && it!=INTSXP && it!=REALSXP && it!=STRSXP)
if (iN && it<LGLSXP && it!=INTSXP && it!=REALSXP && it!=STRSXP)
potential tests to add
coalesce.c:53 this code seems like it is setting values, so maybe the values are not being tested?
switch(TYPEOF(first)) {
switch(TYPEOF(first)) {
break;
dogroups.c:345 looks like an estimate of number of rows returned in j, starts at -1, and incremented from there, so this could be an off by one?
estn = 0;
/*estn = 0;*/
fastmean.c:78 happens for REALSXP, seems like it should be important, why do test still pass with this change?
s += t/n;
s += t*n;
tests added
fread.R:341 added test with fread(key=character()) https://github.com/Rdatatable/data.table/pull/6115
if (length(key) == 1L) {
if (length(key) < 1L) {
below source code
source code I used to create output above is shown below:
> rmq=function(s)gsub('""""','"',s,fixed=TRUE);mutant.dt[,suffix:=sub(".*[.]", "", file)][order(suffix,file,line)][critical==1 & software=="data.table", cat(sprintf("[%s:%d](https://github.com/Rdatatable/data.table/blob/1.15.0/%s/%s#L%d)\n```\n%s\n%s\n```\n", file,line,ifelse(suffix=="R","R","src"),file,line,rmq(original),rmq(mutated)),sep="\n")]
I will be going through these mutants and classifying them based on how bad they are / and then I will create some new test PRs. If anyone else wants to help, that is great.
- Dominant language
- R
- Stars
- 3.9k
- Forks
- 1.1k
- Avg merge
- 14h 4m
- Merged PRs (30d)
- 4
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from Rdatatable/data.table
-
as.data.table() recurses without end on a survival::Surv object (or any data.frame carrying one) Open
Difficulty 2/5 1-3 hours Newbie friendliness 88/100
Rdatatable/data.table#7887 ·
-
consistency tests
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
Rdatatable/data.table#7853 · 3 comments ·
-
internals
Difficulty 2/5 1-3 hours Newbie friendliness 65/100
Rdatatable/data.table#6938 · 1 comment ·
-
encoding fread
Difficulty 2/5 1-3 hours Newbie friendliness 65/100
Rdatatable/data.table#5179 · 8 comments ·
-
documentation programming
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
Rdatatable/data.table#3199 · 3 comments ·
All issues in Rdatatable/data.table
Similar issues
-
Difficulty 2/5 1-3 hours Newbie friendliness 82/100
r-lib/pkgdepends#485 · 3 comments ·
-
Difficulty 1/5 Under an hour Newbie friendliness 92/100
-
beginners blocker
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
-
enviPathR OpenBuild Error Build OK Build Warning policies-accepted pre-review precheck-passed
Difficulty 1/5 Under an hour Newbie friendliness 84/100
Bioconductor/BiocContributions#207 · 6 comments ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 74/100
datacarpentry/semester-biology#1255 ·