Changeset View
Changeset View
Standalone View
Standalone View
src/__tests__/PhutilLibraryTestCase.php
- This file was added.
| <?php | |||||
| /** | |||||
| * @concrete-extensible | |||||
| */ | |||||
| class PhutilLibraryTestCase extends PhutilTestCase { | |||||
| /** | |||||
| * This is more of an acceptance test case instead of a unit test. It verifies | |||||
| * that all symbols can be loaded correctly. It can catch problems like | |||||
| * missing methods in descendants of abstract base classes. | |||||
| */ | |||||
| public function testEverythingImplemented() { | |||||
| id(new PhutilSymbolLoader()) | |||||
| ->setLibrary($this->getLibraryName()) | |||||
| ->selectAndLoadSymbols(); | |||||
| $this->assertTrue(true); | |||||
| } | |||||
| /** | |||||
| * This is more of an acceptance test case instead of a unit test. It verifies | |||||
| * that all the library map is up-to-date. | |||||
| */ | |||||
| public function testLibraryMap() { | |||||
| $root = $this->getLibraryRoot(); | |||||
| $library = phutil_get_library_name_for_root($root); | |||||
| $new_library_map = id(new PhutilLibraryMapBuilder($root)) | |||||
| ->buildMap(); | |||||
| $bootloader = PhutilBootloader::getInstance(); | |||||
| $old_library_map = $bootloader->getLibraryMapWithoutExtensions($library); | |||||
| unset($old_library_map[PhutilLibraryMapBuilder::LIBRARY_MAP_VERSION_KEY]); | |||||
| $identical = ($new_library_map === $old_library_map); | |||||
| if (!$identical) { | |||||
| $differences = $this->getMapDifferences( | |||||
| $old_library_map, | |||||
| $new_library_map); | |||||
| sort($differences); | |||||
| } else { | |||||
| $differences = array(); | |||||
| } | |||||
| $this->assertTrue( | |||||
| $identical, | |||||
| pht( | |||||
| "The library map is out of date. Rebuild it with `%s`.\n". | |||||
| "These entries differ: %s.", | |||||
| 'arc liberate', | |||||
| implode(', ', $differences))); | |||||
| } | |||||
| private function getMapDifferences($old, $new) { | |||||
| $changed = array(); | |||||
| $all = $old + $new; | |||||
| foreach ($all as $key => $value) { | |||||
| $old_exists = array_key_exists($key, $old); | |||||
| $new_exists = array_key_exists($key, $new); | |||||
| // One map has it and the other does not, so mark it as changed. | |||||
| if ($old_exists != $new_exists) { | |||||
| $changed[] = $key; | |||||
| continue; | |||||
| } | |||||
| $oldv = idx($old, $key); | |||||
| $newv = idx($new, $key); | |||||
| if ($oldv === $newv) { | |||||
| continue; | |||||
| } | |||||
| if (is_array($oldv) && is_array($newv)) { | |||||
| $child_changed = $this->getMapDifferences($oldv, $newv); | |||||
| foreach ($child_changed as $child) { | |||||
| $changed[] = $key.'.'.$child; | |||||
| } | |||||
| } else { | |||||
| $changed[] = $key; | |||||
| } | |||||
| } | |||||
| return $changed; | |||||
| } | |||||
| /** | |||||
| * This is more of an acceptance test case instead of a unit test. It verifies | |||||
| * that methods in subclasses have the same visibility as the method in the | |||||
| * parent class. | |||||
| */ | |||||
| public function testMethodVisibility() { | |||||
| $symbols = id(new PhutilSymbolLoader()) | |||||
| ->setLibrary($this->getLibraryName()) | |||||
| ->selectSymbolsWithoutLoading(); | |||||
| $classes = array(); | |||||
| foreach ($symbols as $symbol) { | |||||
| if ($symbol['type'] == 'class') { | |||||
| $classes[$symbol['name']] = new ReflectionClass($symbol['name']); | |||||
| } | |||||
| } | |||||
| $failures = array(); | |||||
| foreach ($classes as $class_name => $class) { | |||||
| $parents = array(); | |||||
| $parent = $class; | |||||
| while ($parent = $parent->getParentClass()) { | |||||
| $parents[] = $parent; | |||||
| } | |||||
| $interfaces = $class->getInterfaces(); | |||||
| foreach ($class->getMethods() as $method) { | |||||
| $method_name = $method->getName(); | |||||
| foreach (array_merge($parents, $interfaces) as $extends) { | |||||
| if ($extends->hasMethod($method_name)) { | |||||
| $xmethod = $extends->getMethod($method_name); | |||||
| if (!$this->compareVisibility($xmethod, $method)) { | |||||
| $failures[] = pht( | |||||
| 'Class "%s" implements method "%s" with the wrong visibility. '. | |||||
| 'The method has visibility "%s", but it is defined in parent '. | |||||
| '"%s" with visibility "%s". In Phabricator, a method which '. | |||||
| 'overrides another must always have the same visibility.', | |||||
| $class_name, | |||||
| $method_name, | |||||
| $this->getVisibility($method), | |||||
| $extends->getName(), | |||||
| $this->getVisibility($xmethod)); | |||||
| } | |||||
| // We found a declaration somewhere, so stop looking. | |||||
| break; | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| $this->assertTrue( | |||||
| empty($failures), | |||||
| "\n\n".implode("\n\n", $failures)); | |||||
| } | |||||
| /** | |||||
| * Get the name of the library currently being tested. | |||||
| */ | |||||
| protected function getLibraryName() { | |||||
| return phutil_get_library_name_for_root($this->getLibraryRoot()); | |||||
| } | |||||
| /** | |||||
| * Get the root directory for the library currently being tested. | |||||
| */ | |||||
| protected function getLibraryRoot() { | |||||
| $caller = id(new ReflectionClass($this))->getFileName(); | |||||
| return phutil_get_library_root_for_path($caller); | |||||
| } | |||||
| private function compareVisibility( | |||||
| ReflectionMethod $parent_method, | |||||
| ReflectionMethod $method) { | |||||
| static $bitmask; | |||||
| if ($bitmask === null) { | |||||
| $bitmask = ReflectionMethod::IS_PUBLIC; | |||||
| $bitmask += ReflectionMethod::IS_PROTECTED; | |||||
| $bitmask += ReflectionMethod::IS_PRIVATE; | |||||
| } | |||||
| $parent_modifiers = $parent_method->getModifiers(); | |||||
| $modifiers = $method->getModifiers(); | |||||
| return !(($parent_modifiers ^ $modifiers) & $bitmask); | |||||
| } | |||||
| private function getVisibility(ReflectionMethod $method) { | |||||
| if ($method->isPrivate()) { | |||||
| return 'private'; | |||||
| } else if ($method->isProtected()) { | |||||
| return 'protected'; | |||||
| } else { | |||||
| return 'public'; | |||||
| } | |||||
| } | |||||
| } | |||||