Skip to content

Instantly share code, notes, and snippets.

@Ladsgroup
Last active October 6, 2019 16:49
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Ladsgroup/bfb4f4b66384efb8b8d3efea338b6e6b to your computer and use it in GitHub Desktop.
Save Ladsgroup/bfb4f4b66384efb8b8d3efea338b6e6b to your computer and use it in GitHub Desktop.
phpunit4 killer
# License: MIT
# Run like: "python phpunit4_killer.py /var/lib/mediawiki/extensions/Wikibase/"
import os
import sys
import re
import subprocess
def find_files(path):
files = []
for r, d, f in os.walk(path):
for file_name in f:
if file_name.endswith('.php'):
files.append(os.path.join(r, file_name))
return files
def check_file(path):
with open(path, 'r', encoding="utf-8") as f:
content = f.read()
old_content = content
cases = re.findall(r'\n\t*?use \\?PHPUnit4And6Compat;', content)
literal = '\'(?:[^\'\\\\]|\\\\\')+\'|"(?:[^"\\\\]|\\\\")+"'
clname = '(?:[a-zA-Z\\\\]+::class|\$\w+|' + literal + ')'
content = re.sub(r'(\$this->)setExpected(Exception\(\s+' + clname + '\s+\);)', r'\1expect\2', content)
content = re.sub(r'^(\\t*)(\$this->)setExpectedException\(\s+(' + clname + '),\s+(' + clname + ')\s+\);', r'\1\2expectException( \3 );\n\1\$this->expectExceptionMessage( \4 );', content)
content = re.sub(r'(\$this->)get(Mock\(\s+' + clname + '\s+\))', r'\1create\2', content)
content = re.sub(r'(\$this->)getMock\(\s+(' + clname + '),\s+\[\],\s+\[\],\s+[\'"]{2},\s+false\s+\)', r'\1createMock( \2 )', content)
content = re.sub(r'^(\\t*)(\$this->)getMock\(\s+(' + clname + '),\s+(\[(?:\s*' + literal + ',?)+\])\s+\);', r'\1\2createMock( \3 )\n\1\t->setMethods( \4 )\n\1\t->getMock();', content)
if '$this->getMock(' in content or '$this->setExpectedException(' in content:
print('Nooooo /o\\')
with open(path, 'w', encoding="utf-8") as f:
print(path)
f.write(content)
return
for case in cases:
content = content.replace(case, '')
content = content.replace(', PHPUnit4And6Compat, ', ', ')
content = content.replace('use PHPUnit4And6Compat, ', 'use ')
content = content.replace(', PHPUnit4And6Compat;', ';')
if content == old_content:
return
with open(path, 'w', encoding="utf-8") as f:
print(path)
f.write(content)
for f in find_files(sys.argv[1]):
check_file(f)
subprocess.run(["composer", "update"], cwd=sys.argv[1], shell=True)
subprocess.run(["composer", "fix"], cwd=sys.argv[1], shell=True)
@Daimona
Copy link

Daimona commented Oct 6, 2019

Line 21: you can replace it with
content = re.sub(r'(\$this->)setExpected(Exception\( (?:[a-zA-Z\\]+::class|\$\w+) \);)', r'\1expect\2', content)
to take variables into account. Note this is still leaving plain literals (and edge cases) out. But for the former, it's probably a good idea to fix them manually AND replace them with MyClass::class.

Line 22: Likewise, it can be replaced with
content = re.sub(r"""^(\t*)(\$this->)setExpectedException\(\s+([a-zA-Z\\]+::class|\$\w+),\s+('(?:[^'\\]|\\')+'|"(?:[^"\\]|\\")+"|\$\w+)\s+\);""", r'\1\2expectException( \3 );\n\1\$this->expectExceptionMessage( \4 );', content)

Line 23: In theory, the semicolon after the closing parentheses is not necessary (e.g. to catch some_func( $this->getMock( MyClass::class ) )).

Lines 20, 31-33: I'd suggest not doing it here. Per wikitech-l, those should be handled together with killing CoversValidator, changing inheritance, and making sure that the directory structure isn't tested. Or we could to that here, too, as long as we do it all together.

@Daimona
Copy link

Daimona commented Oct 6, 2019

Some other improvements (includes the ones suggested above).

Before line 21, add:

literal = '\'(?:[^\'\\\\]|\\\\\')+\'|"(?:[^"\\\\]|\\\\")+"'
clname = '(?:[a-zA-Z\\\\]+::class|\$\w+|' + literal + ')'

Line21 -->content = re.sub(r'(\$this->)setExpected(Exception\(\s+' + clname + '\s+\);)', r'\1expect\2', content)

Line22 -->content = re.sub(r'^(\\t*)(\$this->)setExpectedException\(\s+(' + clname + '),\s+(' + clname + ')\s+\);', r'\1\2expectException( \3 );\n\1\$this->expectExceptionMessage( \4 );', content)

Line23 -->content = re.sub(r'(\$this->)get(Mock\(\s+' + clname + '\s+\))', r'\1create\2', content)

And after line 23:

content = re.sub(r'(\$this->)get(Mock\(\s+' + clname + '),\s+\[\],\s+\[\],\s+[\'"]{2},\s+false\s+\)', r'\1create\2 )', content)
content = re.sub(r'^(\\t*)(\$this->)get(Mock\(\s+' + clname + '),\s+(\[(?:\s*' + literal + ',?)+\])\s+\);', r'\1\2create\3 )\n\1\t->setMethods( \4 )\n\1\t->getMock();', content)

These will still leave some edge cases (e.g.: func call, arrays, class members, string concat), but we can fix them manually. We should also replace literal class names with ::class, but I didn't add it because you'd also have to use the classes etc.

Note: Untested

@Ladsgroup
Copy link
Author

Applied your suggestions, regarding removing phpunit trait, I will explain it further in the ticket.

@Daimona
Copy link

Daimona commented Oct 6, 2019

Cool, thanks :) I'm gonna (ab)use it then

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment