Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
tdhock opened this issue May 1, 2024 · 6 comments
Open

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

tdhock opened this issue May 1, 2024 · 6 comments
Labels
Needs Tests Unit test(s) needed to prevent regressions

Comments

@tdhock
Copy link
Contributor

tdhock commented May 1, 2024

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

@tdhock tdhock added Bug Needs Triage Issue that has not been reviewed by a pandas team member labels May 1, 2024
@rhshadrach rhshadrach added Needs Tests Unit test(s) needed to prevent regressions and removed Needs Triage Issue that has not been reviewed by a pandas team member Bug labels May 1, 2024
@jsngn
Copy link
Contributor

jsngn commented Aug 21, 2024

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
Copy link
Contributor

jsngn commented Sep 4, 2024

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
Copy link
Contributor

jsngn commented Sep 7, 2024

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
Copy link
Contributor Author

tdhock commented Sep 8, 2024

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, Rdatatable/data.table#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.

@jsngn
Copy link
Contributor

jsngn commented Dec 3, 2024

Hey @tdhock, thank you for making the categories, I will continue working on this and open PRs as necessary. Although, I'm not able to edit your comment, so maybe I can just specify any item to ignore I encounter in a comment and tag you?

@tdhock
Copy link
Contributor Author

tdhock commented Dec 4, 2024

hmm, it would probably be better to move the list to a place where anybody could edit... but I don't see a wiki here either...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Needs Tests Unit test(s) needed to prevent regressions
Projects
None yet
Development

No branches or pull requests

3 participants