pandas-dev / pandas

Flexible and powerful data analysis / manipulation library for Python, providing labeled data structures similar to R data.frame objects, statistical functions, and much more
https://pandas.pydata.org
BSD 3-Clause "New" or "Revised" License
43.74k stars 17.95k forks source link

TST: mutation testing results (possible tests to add) #58517

Open tdhock opened 6 months ago

tdhock commented 6 months ago

Hi, @agroce and I did mutation testing of pandas, and here are some significant changes/mutants in pandas source code lines that were apparently covered by tests, but when we ran the tests with the mutation, the tests passed, so pandas may want to add tests that would fail for these changes/mutants.

This was run on v2.2.1 so the line numbers/links refer to that version of the code (first line original, second line mutated).

I will be going through these mutants, trying to identify significant ones, and then hopefully filing PRs to add relevant tests. If anyone else has time to spare, please help by looking at the above mutations, and creating corresponding test cases, to help improve the pandas test suite.

already filed prs

core/arrays/interval.py:866

def min(self, *, axis: AxisInt | None = None, skipna: bool = True) -> IntervalOrNA:
def min(self, *, axis: AxisInt | None = None, skipna: bool = False) -> IntervalOrNA:

core/arrays/sparse/array.py:1768

if len(self) != len(other):
if len(self) > len(other):

core/arrays/boolean.py:379

if not other_is_scalar and len(self) != len(other):
if not other_is_scalar and len(self) > len(other):

to investigate

core/algorithms.py:1592

null_locs = null_pos.nonzero()[0]
null_locs = null_pos.nonzero()[-1]

core/apply.py:962

if self.axis == 0:
if self.axis <= 0:

core/array_algos/take.py:254

col_mask = col_idx == -1
col_mask = col_idx < -1

core/arrays/arrow/array.py:1501

if dropna and data.null_count > 0:
if dropna and data.null_count > -1:

core/arrays/arrow/array.py:2120

return pc.if_else(cond, left, right)
pass

core/arrays/arrow/array.py:2167

if isinstance(values, pa.ChunkedArray) and pa.types.is_boolean(values.type):
if isinstance(values, pa.ChunkedArray)  or pa.types.is_boolean(values.type):

core/arrays/boolean.py:209

if (inferred_dtype in integer_like) and not (
if True and not (

core/arrays/categorical.py:2079

elif is_any_real_numeric_dtype(self.categories.dtype):
elif not (is_any_real_numeric_dtype(self.categories.dtype)):

core/arrays/categorical.py:2743

assert self.ordered  # checked earlier
pass

core/arrays/datetimes.py:283

if lib.infer_dtype(scalars, skipna=True) not in ["datetime", "datetime64"]:
if lib.infer_dtype(scalars, skipna=True) not in [""]:

core/arrays/datetimes.py:2271

data_dtype = data.dtype
pass

core/arrays/datetimes.py:2346

data = data.reshape(shape)
pass

core/arrays/timedeltas.py:376

start_i = i * chunksize
start_i = i % chunksize

core/frame.py:6451

level_values = lib.maybe_convert_objects(level_values)
pass

core/generic.py:6628

if isinstance(dtype, ExtensionDtype) and all(
if True and all(

core/generic.py:8664

self = cast("Series", self)
pass

core/generic.py:11199

axis = self._get_axis_number(axis)
pass

core/generic.py:11689

if nonexistent not in nonexistent_options and not isinstance(
if nonexistent not in nonexistent_options and  isinstance(

core/generic.py:12637

axis = self._get_axis_number(axis)
pass

core/groupby/groupby.py:5658

if len(in_axis_grps) > 0:
if len(in_axis_grps) > 1:

core/groupby/ops.py:785

return self.groupings[0]._result_index.rename(self.names[0])
return self.groupings[0]._result_index.rename(self.names[-1])

core/indexes/base.py:5640

return False
pass

core/indexes/base.py:7180

return arr
pass

core/indexes/multi.py:2141

na_idx = np.where(uniques == -1)[0]
na_idx = np.where(uniques == -1)[-1]

core/indexes/multi.py:3704

if len(self) != len(other):
if len(self) > len(other):

core/indexes/range.py:753

elif len(other) == 1:
elif len(other) == -1:

core/indexes/range.py:771

return type(self)(start_r, end_r + step_s / 2, step_s / 2)
pass

core/indexing.py:2018

elif len(ilocs) == 1 and com.is_null_slice(pi) and len(self.obj) == 0:
elif len(ilocs) <= 1 and com.is_null_slice(pi) and len(self.obj) == 0:

core/internals/base.py:359

if isinstance(value, np.ndarray) and value.ndim == 1 and len(value) == 1:
if isinstance(value, np.ndarray) and value.ndim > 1 and len(value) == 1:

core/internals/blocks.py:2454

elif lib.is_integer(indexer[1]) and indexer[1] == 0:
elif lib.is_integer(indexer[-1]) and indexer[1] == 0:

core/internals/managers.py:217

bp = BlockPlacement(slice(0, 0))
bp = BlockPlacement(slice(0, 1))

core/internals/managers.py:1542

assert self.ndim >= 2
assert self.ndim >= -1

core/internals/managers.py:2074

self.blocks[0]._mgr_locs = BlockPlacement(slice(len(values)))
self.blocks[-1]._mgr_locs = BlockPlacement(slice(len(values)))

core/methods/describe.py:161

if obj.ndim == 2 and obj.columns.size == 0:
if obj.ndim != 2 and obj.columns.size == 0:

core/resample.py:285

return super().pipe(func, *args, **kwargs)
return super().pipe(func, **kwargs)

core/reshape/merge.py:1661

elif validate in ["one_to_many", "1:m"]:
elif validate in ["one_to_many"]:

core/reshape/merge.py:2100

if isinstance(left.dtype, CategoricalDtype) and isinstance(
if True and isinstance(

core/reshape/merge.py:2156

if self.tolerance < Timedelta(0):
if self.tolerance < Timedelta(-1):

core/reshape/merge.py:2671

return _get_join_keys(llab, rlab, shape, sort)
return _get_join_keys( rlab,llab, shape, sort)

core/reshape/reshape.py:489

def unstack(obj: Series | DataFrame, level, fill_value=None, sort: bool = True):
def unstack(obj: Series | DataFrame, level, fill_value=None, sort: bool = False):

core/reshape/reshape.py:678

level = [v if v <= lev else v - 1 for v in level]
level = [v if v < lev else v - 1 for v in level]

core/reshape/tile.py:387

mx += 0.001 * abs(mx) if mx != 0 else 0.001
mx += 0.001 / abs(mx) if mx != 0 else 0.001

core/window/ewm.py:387

if common.count_not_none(self.com, self.span, self.alpha) > 0:
if common.count_not_none(self.com, self.span, self.alpha) < 0:

To ignore (probably not significant)

these mutants involve non-significant permutation of arguments.

core/window/ewm.py:858 ignore since this function is symmetric.

def cov_func(x, y):
def cov_func( y,x):

core/apply.py:1441

result = obj.apply(func, args=self.args, **self.kwargs)
result = obj.apply(func, **self.kwargs, args=self.args)

core/arrays/arrow/array.py:136

has_remainder = pc.not_equal(pc.multiply(divided, right), left)
has_remainder = pc.not_equal(pc.multiply( right,divided), left)

core/arrays/arrow/extension_types.py:108

return hash((str(self), str(self.subtype), self.closed))
return hash((str(self), self.closed, str(self.subtype)))

Code below was used to produce the output above:

> rmq=function(s)gsub('""""','"',s,fixed=TRUE);mutant.dt[,suffix:=sub(".*[.]", "", file)][order(suffix,file,line)][critical==1 & software=="pandas", cat(sprintf("[%s:%d](https://github.com/pandas-dev/pandas/blob/v2.2.1/pandas/%s#L%d)\n```\n%s\n%s\n```\n", file,line,file,line,rmq(original),rmq(mutated)),sep="\n")]

already investigated, but not likely to result in Prs (please write why not)

TODO

jsngn commented 2 months ago

I can add/improve tests for

core/arrays/sparse/array.py:1768

if len(self) != len(other):
if len(self) > len(other):

since it looks like that case is still passing when it shouldn't. And if that goes well then I can look into some of the other ones :)

jsngn commented 2 months ago

I can take this next (confirmed tests are still passing incorrectly on main)

core/arrays/boolean.py:379

if not other_is_scalar and len(self) != len(other):
if not other_is_scalar and len(self) > len(other):
jsngn commented 2 months ago

I'll open a PR for core/arrays/interval.py:866

def min(self, *, axis: AxisInt | None = None, skipna: bool = True) -> IntervalOrNA:
def min(self, *, axis: AxisInt | None = None, skipna: bool = False) -> IntervalOrNA:
tdhock commented 2 months ago

hi @jsngn I'm glad to see that these results have been useful.

I edited my original/first comment in this PR to add several sections, to organize the mutants, according to which ones have already corresponding PRs, similar to what we did in data.table, https://github.com/Rdatatable/data.table/issues/6114

I also added a section "already investigated, but not likely to result in Prs (please write why not)" so please feel free to edit my comment yourself, and move items that you have investigated down into that section, and write why they are unlikely to result in Prs. This is similar to the "To ignore (probably not significant)" section which involves permutation of arguments.