Page Menu
Home
WickedGov Phorge
Search
Configure Global Search
Log In
Files
F1425790
editTests.php
No One
Temporary
Actions
Download File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Size
14 KB
Referenced Files
None
Subscribers
None
editTests.php
View Options
<?php
use
MediaWiki\Maintenance\Maintenance
;
use
MediaWiki\Settings\SettingsBuilder
;
use
MediaWiki\Tests\AnsiTermColorer
;
use
Wikimedia\Diff\Diff
;
use
Wikimedia\Diff\UnifiedDiffFormatter
;
use
Wikimedia\Parsoid\ParserTests\Test
as
ParserTest
;
use
Wikimedia\Parsoid\ParserTests\TestFileReader
;
use
Wikimedia\Parsoid\ParserTests\TestMode
as
ParserTestMode
;
use
Wikimedia\ScopedCallback
;
require_once
__DIR__
.
'/../../maintenance/Maintenance.php'
;
define
(
'MW_AUTOLOAD_TEST_CLASSES'
,
true
);
/**
* Interactive parser test runner and test file editor
*/
class
ParserEditTests
extends
Maintenance
{
/** @var int */
private
$termWidth
;
/** @var TestFileReader[] */
private
$testFiles
;
/** @var int */
private
$testCount
;
/** @var TestRecorder */
private
$recorder
;
/** @var ParserTestRunner */
private
$runner
;
/** @var int */
private
$numExecuted
;
/** @var int */
private
$numSkipped
;
/** @var int */
private
$numFailed
;
/** @var array */
private
$results
;
/** @var array */
private
$session
;
public
function
__construct
()
{
parent
::
__construct
();
$this
->
addOption
(
'session-data'
,
'internal option, do not use'
,
false
,
true
);
}
public
function
finalSetup
(
SettingsBuilder
$settingsBuilder
)
{
// Some methods which are discouraged for normal code throw exceptions unless
// we declare this is just a test.
define
(
'MW_PARSER_TEST'
,
true
);
parent
::
finalSetup
(
$settingsBuilder
);
TestSetup
::
applyInitialConfig
();
}
public
function
execute
()
{
$this
->
termWidth
=
$this
->
getTermSize
()[
0
]
-
1
;
$this
->
recorder
=
new
TestRecorder
();
$this
->
setupFileData
();
if
(
$this
->
hasOption
(
'session-data'
)
)
{
$this
->
session
=
json_decode
(
$this
->
getOption
(
'session-data'
),
true
);
}
else
{
$this
->
session
=
[
'options'
=>
[]
];
}
// @phan-suppress-next-line PhanTypeArraySuspiciousNullable options always set
$this
->
runner
=
new
ParserTestRunner
(
$this
->
recorder
,
$this
->
session
[
'options'
]
);
$this
->
runTests
();
if
(
$this
->
numFailed
===
0
)
{
if
(
$this
->
numSkipped
===
0
)
{
print
"All tests passed!
\n
"
;
}
else
{
print
"All tests passed (but skipped {$this->numSkipped})
\n
"
;
}
return
;
}
print
"{$this->numFailed} test(s) failed.
\n
"
;
$this
->
showResults
();
}
protected
function
setupFileData
()
{
$this
->
testFiles
=
[];
$this
->
testCount
=
0
;
foreach
(
ParserTestRunner
::
getParserTestFiles
()
as
$file
)
{
$fileInfo
=
TestFileReader
::
read
(
$file
);
$this
->
testFiles
[
$file
]
=
$fileInfo
;
$this
->
testCount
+=
count
(
$fileInfo
->
testCases
);
}
}
protected
function
getTestDesc
(
ParserTest
$test
)
{
return
$test
->
testName
;
// could include mode here too
}
protected
function
getResultSection
(
ParserTest
$test
)
{
// This used to switch between html and html+tidy, but we
// got rid of the "notidy" support some time ago.
// This should probably eventually support html+standalone
// and html+integrated in a similar way, but for now just
// walk the legacy HTML fallback set
$legacyHtmlKeys
=
[
'html/php'
,
'html/*'
,
'html'
,
# deprecated
'result'
,
'html/php+tidy'
,
'html/*+tidy'
,
'html+tidy'
,
];
foreach
(
$legacyHtmlKeys
as
$key
)
{
if
(
$test
->
sections
[
$key
]
??
false
)
{
return
$key
;
}
}
return
'html'
;
}
protected
function
runTests
()
{
$teardownGuard
=
null
;
$teardownGuard
=
$this
->
runner
->
setupDatabase
(
$teardownGuard
);
$teardownGuard
=
$this
->
runner
->
staticSetup
(
$teardownGuard
);
$teardownGuard
=
$this
->
runner
->
setupUploads
(
$teardownGuard
);
print
"Running tests...
\n
"
;
$this
->
results
=
[];
$this
->
numExecuted
=
0
;
$this
->
numSkipped
=
0
;
$this
->
numFailed
=
0
;
$mode
=
new
ParserTestMode
(
'legacy'
);
foreach
(
$this
->
testFiles
as
$fileName
=>
$fileInfo
)
{
$this
->
runner
->
addArticles
(
$fileInfo
->
articles
);
foreach
(
$fileInfo
->
testCases
as
$testInfo
)
{
$result
=
$this
->
runner
->
runTest
(
$testInfo
,
$mode
);
if
(
$result
===
false
)
{
$this
->
numSkipped
++;
}
elseif
(
!
$result
->
isSuccess
()
)
{
$desc
=
$this
->
getTestDesc
(
$testInfo
);
$this
->
results
[
$fileName
][
$desc
]
=
$result
;
$this
->
numFailed
++;
}
$this
->
numExecuted
++;
$this
->
showProgress
();
}
}
print
"
\n
"
;
ScopedCallback
::
consume
(
$teardownGuard
);
}
protected
function
showProgress
()
{
$done
=
$this
->
numExecuted
;
$total
=
$this
->
testCount
;
$width
=
$this
->
termWidth
-
9
;
$pos
=
(
int
)
round
(
$width
*
$done
/
$total
);
printf
(
'│'
.
str_repeat
(
'█'
,
$pos
)
.
str_repeat
(
'-'
,
$width
-
$pos
)
.
"│ %5.1f%%
\r
"
,
$done
/
$total
*
100
);
}
protected
function
showResults
()
{
if
(
isset
(
$this
->
session
[
'startFile'
]
)
)
{
$startFile
=
$this
->
session
[
'startFile'
];
$startTest
=
$this
->
session
[
'startTest'
];
$foundStart
=
false
;
}
else
{
$startFile
=
false
;
$startTest
=
false
;
$foundStart
=
true
;
}
$testIndex
=
0
;
foreach
(
$this
->
testFiles
as
$fileName
=>
$fileInfo
)
{
if
(
!
isset
(
$this
->
results
[
$fileName
]
)
)
{
continue
;
}
if
(
!
$foundStart
&&
$startFile
!==
false
&&
$fileName
!==
$startFile
)
{
$testIndex
+=
count
(
$this
->
results
[
$fileName
]
);
continue
;
}
foreach
(
$fileInfo
->
testCases
as
$testInfo
)
{
$desc
=
$this
->
getTestDesc
(
$testInfo
);
if
(
!
isset
(
$this
->
results
[
$fileName
][
$desc
]
)
)
{
continue
;
}
$result
=
$this
->
results
[
$fileName
][
$desc
];
$testIndex
++;
if
(
!
$foundStart
&&
$startTest
!==
false
)
{
if
(
$desc
!==
$startTest
)
{
continue
;
}
$foundStart
=
true
;
}
$this
->
handleFailure
(
$testIndex
,
$testInfo
,
$result
);
}
}
if
(
!
$foundStart
)
{
print
"Could not find the test after a restart, did you rename it?"
;
unset
(
$this
->
session
[
'startFile'
]
);
unset
(
$this
->
session
[
'startTest'
]
);
// @phan-suppress-next-line PhanPossiblyInfiniteRecursionSameParams
$this
->
showResults
();
}
print
"All done
\n
"
;
}
protected
function
heading
(
$text
)
{
$term
=
new
AnsiTermColorer
;
$heading
=
"─── $text "
;
$heading
.=
str_repeat
(
'─'
,
$this
->
termWidth
-
mb_strlen
(
$heading
)
);
$heading
=
$term
->
color
(
'34'
)
.
$heading
.
$term
->
reset
()
.
"
\n
"
;
return
$heading
;
}
protected
function
unifiedDiff
(
$left
,
$right
)
{
$fromLines
=
explode
(
"
\n
"
,
$left
);
$toLines
=
explode
(
"
\n
"
,
$right
);
$formatter
=
new
UnifiedDiffFormatter
;
return
$formatter
->
format
(
new
Diff
(
$fromLines
,
$toLines
)
);
}
protected
function
handleFailure
(
$index
,
$testInfo
,
$result
)
{
$term
=
new
AnsiTermColorer
;
$div1
=
$term
->
color
(
'34'
)
.
str_repeat
(
'━'
,
$this
->
termWidth
)
.
$term
->
reset
()
.
"
\n
"
;
$div2
=
$term
->
color
(
'34'
)
.
str_repeat
(
'─'
,
$this
->
termWidth
)
.
$term
->
reset
()
.
"
\n
"
;
$desc
=
$this
->
getTestDesc
(
$testInfo
);
print
$div1
;
print
"Failure $index/{$this->numFailed}: {$testInfo->filename} line {$testInfo->lineNumStart}
\n
"
.
"{$desc}
\n
"
;
print
$this
->
heading
(
'Input'
);
print
"{$testInfo->wikitext}
\n
"
;
print
$this
->
heading
(
'Alternating expected/actual output'
);
print
$this
->
alternatingAligned
(
$result
->
expected
,
$result
->
actual
);
print
$this
->
heading
(
'Diff'
);
$dwdiff
=
$this
->
dwdiff
(
$result
->
expected
,
$result
->
actual
);
if
(
$dwdiff
!==
false
)
{
$diff
=
$dwdiff
;
}
else
{
$diff
=
$this
->
unifiedDiff
(
$result
->
expected
,
$result
->
actual
);
}
print
$diff
;
if
(
$testInfo
->
options
||
$testInfo
->
config
)
{
print
$this
->
heading
(
'Options / Config'
);
if
(
$testInfo
->
options
)
{
print
json_encode
(
$testInfo
->
options
)
.
"
\n
"
;
}
if
(
$testInfo
->
config
)
{
print
json_encode
(
$testInfo
->
config
)
.
"
\n
"
;
}
}
print
$div2
;
print
"What do you want to do?
\n
"
;
$specs
=
[
'[R]eload code and run again'
,
'[U]pdate source file, copy actual to expected'
,
'[I]gnore'
];
# XXX originally isSubtest was a way to edit the +tidy vs +untidy
# portions of the test separately (I believe)
// @phan-suppress-next-line PhanUndeclaredProperty
if
(
!
empty
(
$testInfo
->
isSubtest
)
)
{
# FIXME: this is orphan code, will never be true
$specs
[]
=
'Delete [s]ubtest'
;
}
$specs
[]
=
'[D]elete test'
;
$specs
[]
=
'[Q]uit'
;
$options
=
[];
foreach
(
$specs
as
$spec
)
{
if
(
!
preg_match
(
'/^(.*
\[
)(.)(
\]
.*)$/'
,
$spec
,
$m
)
)
{
throw
new
LogicException
(
'Invalid option spec: '
.
$spec
);
}
print
'* '
.
$m
[
1
]
.
$term
->
color
(
'35'
)
.
$m
[
2
]
.
$term
->
color
(
'0'
)
.
$m
[
3
]
.
"
\n
"
;
$options
[
strtoupper
(
$m
[
2
]
)]
=
true
;
}
do
{
$response
=
self
::
readconsole
();
$cmdResult
=
false
;
if
(
$response
===
false
)
{
exit
(
0
);
}
$response
=
strtoupper
(
trim
(
$response
)
);
if
(
!
isset
(
$options
[
$response
]
)
)
{
print
"Invalid response, please enter a single letter from the list above
\n
"
;
continue
;
}
switch
(
strtoupper
(
trim
(
$response
)
)
)
{
case
'R'
:
$cmdResult
=
$this
->
reload
(
$testInfo
);
break
;
case
'U'
:
$cmdResult
=
$this
->
update
(
$testInfo
,
$result
);
break
;
case
'I'
:
return
;
case
'T'
:
$cmdResult
=
$this
->
switchTidy
(
$testInfo
);
break
;
case
'S'
:
$cmdResult
=
$this
->
deleteSubtest
(
$testInfo
);
break
;
case
'D'
:
$cmdResult
=
$this
->
deleteTest
(
$testInfo
);
break
;
case
'Q'
:
exit
(
0
);
}
}
while
(
!
$cmdResult
);
}
protected
function
dwdiff
(
$expected
,
$actual
)
{
if
(
!
is_executable
(
'/usr/bin/dwdiff'
)
)
{
return
false
;
}
$markers
=
[
"
\n
"
=>
'¶'
,
' '
=>
'·'
,
"
\t
"
=>
'→'
];
$markedExpected
=
strtr
(
$expected
,
$markers
);
$markedActual
=
strtr
(
$actual
,
$markers
);
$diff
=
$this
->
unifiedDiff
(
$markedExpected
,
$markedActual
);
$tempFile
=
tmpfile
();
fwrite
(
$tempFile
,
$diff
);
fseek
(
$tempFile
,
0
);
$pipes
=
[];
$proc
=
proc_open
(
'/usr/bin/dwdiff -Pc --diff-input'
,
[
0
=>
$tempFile
,
1
=>
[
'pipe'
,
'w'
],
2
=>
STDERR
],
$pipes
);
if
(
!
$proc
)
{
return
false
;
}
$result
=
stream_get_contents
(
$pipes
[
1
]
);
proc_close
(
$proc
);
fclose
(
$tempFile
);
return
$result
;
}
protected
function
alternatingAligned
(
$expectedStr
,
$actualStr
)
{
$expectedLines
=
explode
(
"
\n
"
,
$expectedStr
);
$actualLines
=
explode
(
"
\n
"
,
$actualStr
);
$maxLines
=
max
(
count
(
$expectedLines
),
count
(
$actualLines
)
);
$result
=
''
;
for
(
$i
=
0
;
$i
<
$maxLines
;
$i
++
)
{
if
(
$i
<
count
(
$expectedLines
)
)
{
$expectedLine
=
$expectedLines
[
$i
];
$expectedChunks
=
str_split
(
$expectedLine
,
$this
->
termWidth
-
3
);
}
else
{
$expectedChunks
=
[];
}
if
(
$i
<
count
(
$actualLines
)
)
{
$actualLine
=
$actualLines
[
$i
];
$actualChunks
=
str_split
(
$actualLine
,
$this
->
termWidth
-
3
);
}
else
{
$actualChunks
=
[];
}
$maxChunks
=
max
(
count
(
$expectedChunks
),
count
(
$actualChunks
)
);
for
(
$j
=
0
;
$j
<
$maxChunks
;
$j
++
)
{
if
(
isset
(
$expectedChunks
[
$j
]
)
)
{
$result
.=
"E: "
.
$expectedChunks
[
$j
];
if
(
$j
===
count
(
$expectedChunks
)
-
1
)
{
$result
.=
"¶"
;
}
$result
.=
"
\n
"
;
}
else
{
$result
.=
"E:
\n
"
;
}
$result
.=
"
\3
3[4m"
.
// underline
"A: "
;
if
(
isset
(
$actualChunks
[
$j
]
)
)
{
$result
.=
$actualChunks
[
$j
];
if
(
$j
===
count
(
$actualChunks
)
-
1
)
{
$result
.=
"¶"
;
}
}
$result
.=
"
\3
3[0m
\n
"
;
// reset
}
}
return
$result
;
}
protected
function
reload
(
$testInfo
)
{
global
$argv
;
pcntl_exec
(
PHP_BINARY
,
[
$argv
[
0
],
'--session-data'
,
json_encode
(
[
'startFile'
=>
$testInfo
->
filename
,
'startTest'
=>
$this
->
getTestDesc
(
$testInfo
),
]
+
$this
->
session
)
]
);
print
"pcntl_exec() failed
\n
"
;
return
false
;
}
protected
function
findTest
(
$file
,
$testInfo
)
{
$initialPart
=
''
;
for
(
$i
=
1
;
$i
<
$testInfo
->
lineNumStart
;
$i
++
)
{
$line
=
fgets
(
$file
);
if
(
$line
===
false
)
{
print
"Error reading from file
\n
"
;
return
false
;
}
$initialPart
.=
$line
;
}
$line
=
fgets
(
$file
);
if
(
!
preg_match
(
'/^!!
\s
*test/'
,
$line
)
)
{
print
"Test has moved, cannot edit
\n
"
;
return
false
;
}
$testPart
=
$line
;
$desc
=
fgets
(
$file
);
if
(
trim
(
$desc
)
!==
$this
->
getTestDesc
(
$testInfo
)
)
{
print
"Description does not match, cannot edit
\n
"
;
return
false
;
}
$testPart
.=
$desc
;
return
[
$initialPart
,
$testPart
];
}
protected
function
getOutputFileName
(
$inputFileName
)
{
if
(
is_writable
(
$inputFileName
)
)
{
$outputFileName
=
$inputFileName
;
}
else
{
$outputFileName
=
wfTempDir
()
.
'/'
.
basename
(
$inputFileName
);
print
"Cannot write to input file, writing to $outputFileName instead
\n
"
;
}
return
$outputFileName
;
}
protected
function
editTest
(
$fileName
,
$deletions
,
$changes
)
{
$text
=
file_get_contents
(
$fileName
);
if
(
$text
===
false
)
{
print
"Unable to open test file!"
;
return
false
;
}
$result
=
TestFileEditor
::
edit
(
$text
,
$deletions
,
$changes
,
static
function
(
$msg
)
{
print
"$msg
\n
"
;
}
);
if
(
is_writable
(
$fileName
)
)
{
file_put_contents
(
$fileName
,
$result
);
print
"Wrote updated file
\n
"
;
}
else
{
print
"Cannot write updated file, here is a patch you can paste:
\n\n
"
;
print
"--- {$fileName}
\n
"
.
"+++ {$fileName}~
\n
"
.
$this
->
unifiedDiff
(
$text
,
$result
)
.
"
\n
"
;
}
}
protected
function
update
(
$testInfo
,
$result
)
{
$resultSection
=
$this
->
getResultSection
(
$testInfo
);
$this
->
editTest
(
$testInfo
->
filename
,
[],
// deletions
[
// changes
$testInfo
->
testName
=>
[
$resultSection
=>
[
'op'
=>
'update'
,
'value'
=>
$result
->
actual
.
"
\n
"
]
]
]
);
return
false
;
}
protected
function
deleteTest
(
$testInfo
)
{
$this
->
editTest
(
$testInfo
->
filename
,
[
$testInfo
->
testName
],
// deletions
[]
// changes
);
return
false
;
}
protected
function
switchTidy
(
$testInfo
)
{
$resultSection
=
$this
->
getResultSection
(
$testInfo
);
if
(
in_array
(
$resultSection
,
[
'html/php'
]
)
)
{
$newSection
=
'html/php'
;
}
elseif
(
in_array
(
$resultSection
,
[
'html/*'
,
'html'
,
'result'
]
)
)
{
$newSection
=
'html'
;
}
else
{
print
"Unrecognised result section name
\"
$resultSection
\"
"
;
return
true
;
}
$this
->
editTest
(
$testInfo
->
filename
,
[],
// deletions
[
// changes
$testInfo
->
testName
=>
[
$resultSection
=>
[
'op'
=>
'rename'
,
'value'
=>
$newSection
]
]
]
);
return
false
;
}
protected
function
deleteSubtest
(
$testInfo
)
{
$resultSection
=
$this
->
getResultSection
(
$testInfo
);
$this
->
editTest
(
$testInfo
->
filename
,
[],
// deletions
[
// changes
$testInfo
->
testName
=>
[
$resultSection
=>
[
'op'
=>
'delete'
]
]
]
);
return
false
;
}
}
$maintClass
=
ParserEditTests
::
class
;
require_once
RUN_MAINTENANCE_IF_MAIN
;
File Metadata
Details
Attached
Mime Type
text/x-php
Expires
Sat, May 16, 12:01 (21 h, 3 m)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
72/0c/f78e5b7c11c4e5765ca084e6ada2
Default Alt Text
editTests.php (14 KB)
Attached To
Mode
rMWPROD MediaWiki Production
Attached
Detach File
Event Timeline
Log In to Comment