#!/usr/bin/env php 0codecept5.pharsrc/Codeception/Actor.php:]g isrc/Codeception/Application.php:]f6src/Codeception/Codecept.php:]yS%src/Codeception/Command/Bootstrap.php:]=d!src/Codeception/Command/Build.php :] !src/Codeception/Command/Clean.php:]&src/Codeception/Command/Completion.php :] YJ.src/Codeception/Command/CompletionFallback.php:]=#e*src/Codeception/Command/ConfigValidate.php:]K8?ʶ#src/Codeception/Command/Console.phpU:]U3|"src/Codeception/Command/DryRun.php:].(src/Codeception/Command/GenerateCept.php:](src/Codeception/Command/GenerateCest.php:]"sm/src/Codeception/Command/GenerateEnvironment.php:]%+src/Codeception/Command/GenerateFeature.php:]eea)src/Codeception/Command/GenerateGroup.phps:]s%g*src/Codeception/Command/GenerateHelper.php:]B.src/Codeception/Command/GeneratePageObject.php:]:-src/Codeception/Command/GenerateScenarios.php:]񴈶,src/Codeception/Command/GenerateSnapshot.phpk:]kK.src/Codeception/Command/GenerateStepObject.php' :]' I%T϶)src/Codeception/Command/GenerateSuite.php:](src/Codeception/Command/GenerateTest.php:]!Le;+src/Codeception/Command/GherkinSnippets.php{ :]{ ay(src/Codeception/Command/GherkinSteps.phpH:]HH5q src/Codeception/Command/Init.php:]"src/Codeception/Command/Run.phpY:]YK&src/Codeception/Command/SelfUpdate.phpS":]S"늶)src/Codeception/Command/Shared/Config.php:]lD-src/Codeception/Command/Shared/FileSystem.php:]pd?.(src/Codeception/Command/Shared/Style.php:]eAO!src/Codeception/Configuration.phpM_:]M_@#src/Codeception/Coverage/Filter.phpq:]q˥&-src/Codeception/Coverage/Subscriber/Local.phpr:]re3src/Codeception/Coverage/Subscriber/LocalServer.php#:]#ϩ +/src/Codeception/Coverage/Subscriber/Printer.phpq:]q4src/Codeception/Coverage/Subscriber/RemoteServer.phpX :]X ,src/Codeception/Coverage/SuiteSubscriber.php7 :]7 *src/Codeception/CustomCommandInterface.php:]gsrc/Codeception/Lib/Connector/ZF2/PersistentServiceManager.php:]Ev%src/Codeception/Lib/Connector/ZF2.php5:]5)src/Codeception/Lib/Console/Colorizer.phpQ:]Qz +src/Codeception/Lib/Console/DiffFactory.phpB:]Bj'src/Codeception/Lib/Console/Message.php& :]& )X.src/Codeception/Lib/Console/MessageFactory.php:]߻W&src/Codeception/Lib/Console/Output.php :] cJ¶#src/Codeception/Lib/DbPopulator.php,:],mҶsrc/Codeception/Lib/Di.php :] ](src/Codeception/Lib/Driver/AmazonSQS.phpZ:]ZU(src/Codeception/Lib/Driver/Beanstalk.php::]:Ѷ!src/Codeception/Lib/Driver/Db.php!:]!Y#src/Codeception/Lib/Driver/Iron.php :] T c&src/Codeception/Lib/Driver/MongoDb.php:]C܊$src/Codeception/Lib/Driver/MySql.php:]5"src/Codeception/Lib/Driver/Oci.phpA:]AsM)src/Codeception/Lib/Driver/PostgreSql.phph:]h99%src/Codeception/Lib/Driver/Sqlite.php :] N\3%src/Codeception/Lib/Driver/SqlSrv.phpi :]i G8!src/Codeception/Lib/Framework.php:]r"isrc/Codeception/Lib/Friend.php:][)src/Codeception/Lib/Generator/Actions.php8:]8tѶ'src/Codeception/Lib/Generator/Actor.phpy:]ya&src/Codeception/Lib/Generator/Cept.php:]uA&src/Codeception/Lib/Generator/Cest.php:]%鵶)src/Codeception/Lib/Generator/Feature.php:]L<1src/Codeception/Lib/Generator/GherkinSnippets.php:];'src/Codeception/Lib/Generator/Group.phpB:]B+b(src/Codeception/Lib/Generator/Helper.php:]yi6,src/Codeception/Lib/Generator/PageObject.php:]2src/Codeception/Lib/Generator/Shared/Classname.php:]/!*src/Codeception/Lib/Generator/Snapshot.php:] ۶,src/Codeception/Lib/Generator/StepObject.php:]`&src/Codeception/Lib/Generator/Test.phpJ:]J"Mo $src/Codeception/Lib/GroupManager.php:]$src/Codeception/Lib/InnerBrowser.php:]w/src/Codeception/Lib/Interfaces/ActiveRecord.phpA:]ANŶ&src/Codeception/Lib/Interfaces/API.phpb:]bD^Ƕ6src/Codeception/Lib/Interfaces/ConflictsWithModule.php:]hГ-src/Codeception/Lib/Interfaces/DataMapper.phpf:]f?j%src/Codeception/Lib/Interfaces/Db.php< :]< Δ/׶2src/Codeception/Lib/Interfaces/DependsOnModule.php_:]_Ī3src/Codeception/Lib/Interfaces/DoctrineProvider.phpu:]u'1src/Codeception/Lib/Interfaces/ElementLocator.php:]N/src/Codeception/Lib/Interfaces/MultiSession.php:])wb&src/Codeception/Lib/Interfaces/ORM.php?:]?ζ2src/Codeception/Lib/Interfaces/PageSourceSaver.phpT:]TV<߶/src/Codeception/Lib/Interfaces/PartedModule.php:]T(src/Codeception/Lib/Interfaces/Queue.php^:]^^=)src/Codeception/Lib/Interfaces/Remote.php:]bW2src/Codeception/Lib/Interfaces/RequiresPackage.php:]Q2src/Codeception/Lib/Interfaces/ScreenshotSaver.phph:]hh2src/Codeception/Lib/Interfaces/SessionSnapshot.php:]_y&src/Codeception/Lib/Interfaces/Web.phpWm:]Wm]u'src/Codeception/Lib/ModuleContainer.phpG9:]G9$src/Codeception/Lib/Notification.php:]<$src/Codeception/Lib/ParamsLoader.php(:](X=$Ķsrc/Codeception/Lib/Parser.php?:]?}R,src/Codeception/Lib/Shared/LaravelCommon.phpw :]w :<src/Codeception/Module/AMQP.php0:]0)˶src/Codeception/Module/Apc.php:]7%R"src/Codeception/Module/Asserts.php:].%src/Codeception/Module/Cli.php :] @&src/Codeception/Module/DataFactory.phpa#:]a## src/Codeception/Module/Db.phpfx:]fxǥ$src/Codeception/Module/Doctrine2.phpH:]HMwHw%src/Codeception/Module/Filesystem.php.:].9src/Codeception/Module/FTP.php>^:]>^#src/Codeception/Module/Laravel5.php3:]3yO src/Codeception/Module/Lumen.php,?:],?C#src/Codeception/Module/Memcache.php:] "src/Codeception/Module/MongoDb.phpl8:]l8!"src/Codeception/Module/Phalcon.phppN:]pN\!?%src/Codeception/Module/PhpBrowser.php:]?م src/Codeception/Module/Queue.php+:]+F5 src/Codeception/Module/Redis.phpQ:]QA\src/Codeception/Module/REST.phpγ:]γ[X#src/Codeception/Module/Sequence.php :] 'Vsrc/Codeception/Module/SOAP.php49:]49"src/Codeception/Module/Symfony.phpT:]TM$src/Codeception/Module/WebDriver.php8:]8:ܓsrc/Codeception/Module/Yii2.phpe:]e)src/Codeception/Module/ZendExpressive.php :] ٠src/Codeception/Module/ZF2.php:]Ssrc/Codeception/Module.phph":]h"#Ҷsrc/Codeception/Scenario.php:]src/Codeception/Snapshot.php! :]! 9src/Codeception/Step/Action.phpX:]XyM1src/Codeception/Step/Argument/FormattedOutput.php:]5lf2src/Codeception/Step/Argument/PasswordArgument.php:]ѡǶ"src/Codeception/Step/Assertion.phpy:]y֝? src/Codeception/Step/Comment.php :] m"src/Codeception/Step/Condition.phpy:]yk-src/Codeception/Step/ConditionalAssertion.php:]~!src/Codeception/Step/Executor.php:]8 O&src/Codeception/Step/GeneratedStep.php:]@#src/Codeception/Step/Incomplete.php:]}Ȯsrc/Codeception/Step/Meta.php:]Pfsrc/Codeception/Step/Retry.php$ :]$ K>src/Codeception/Step/Skip.php|:]|k@src/Codeception/Step/TryTo.php:] /src/Codeception/Step.php(:](+*src/Codeception/Subscriber/AutoRebuild.php:]zc.src/Codeception/Subscriber/BeforeAfterTest.php:]t(src/Codeception/Subscriber/Bootstrap.phpe:]e炫&src/Codeception/Subscriber/Console.phpgP:]gP&&+src/Codeception/Subscriber/Dependencies.php:]uL&+src/Codeception/Subscriber/ErrorHandler.php:]<5.src/Codeception/Subscriber/ExtensionLoader.php:]P'src/Codeception/Subscriber/FailFast.php:]t82src/Codeception/Subscriber/GracefulTermination.php:]2V%src/Codeception/Subscriber/Module.phpl :]l p*src/Codeception/Subscriber/PrepareTest.phpM:]Mxrc2src/Codeception/Subscriber/Shared/StaticEvents.php:]Ssrc/Codeception/Suite.php:]ݿ src/Codeception/SuiteManager.php:]g'src/Codeception/Template/Acceptance.phpN:]NH;  src/Codeception/Template/Api.php :] +}"&src/Codeception/Template/Bootstrap.phpy:]yB!src/Codeception/Template/Unit.php :] DsI{src/Codeception/Test/Cept.php2:]2}aisrc/Codeception/Test/Cest.php:]=#src/Codeception/Test/Descriptor.phpb:]bnk1src/Codeception/Test/Feature/AssertionCounter.php:]S-src/Codeception/Test/Feature/CodeCoverage.php:]We,src/Codeception/Test/Feature/ErrorLogger.php:] ҥ8src/Codeception/Test/Feature/IgnoreIfMetadataBlocked.php:] B2src/Codeception/Test/Feature/MetadataCollector.php:]Y/src/Codeception/Test/Feature/ScenarioLoader.phpC:]CN< src/Codeception/Test/Gherkin.phpm:]m |-src/Codeception/Test/Interfaces/Dependent.phpo:]oNx/src/Codeception/Test/Interfaces/Descriptive.php:]֙)src/Codeception/Test/Interfaces/Plain.phpn:]nE,src/Codeception/Test/Interfaces/Reported.php:]G"+2src/Codeception/Test/Interfaces/ScenarioDriven.phpD:]D&<2src/Codeception/Test/Interfaces/StrictCoverage.php:]$src/Codeception/Test/Loader/Cept.php:]Nގ$src/Codeception/Test/Loader/Cest.php:]h""n'src/Codeception/Test/Loader/Gherkin.phpr:]r q/src/Codeception/Test/Loader/LoaderInterface.php:]F$src/Codeception/Test/Loader/Unit.php :] vsrc/Codeception/Test/Loader.php^:]^!src/Codeception/Test/Metadata.php:]* src/Codeception/Test/Test.php :] 5жsrc/Codeception/Test/Unit.php:]ͿZ=!src/Codeception/TestInterface.php:]'src/Codeception/Util/ActionSequence.php :] 2#src/Codeception/Util/Annotation.php4:]40src/Codeception/Util/ArrayContainsComparator.php:]ڶ!src/Codeception/Util/Autoload.php:]ȷ'src/Codeception/Util/Debug.php:]ۼr#src/Codeception/Util/FileSystem.php; :]; W!src/Codeception/Util/Fixtures.php,:],mDʶ!src/Codeception/Util/HttpCode.php:] ʂ"src/Codeception/Util/JsonArray.php1:]1ts!src/Codeception/Util/JsonType.phpd!:]d!m src/Codeception/Util/Locator.php3(:]3(۪src/Codeception/Util/Maybe.phpF:]F`/%src/Codeception/Util/PathResolver.php:]fq)src/Codeception/Util/ReflectionHelper.php:]s'src/Codeception/Util/Shared/Asserts.php"=:]"=TI*src/Codeception/Util/Shared/Namespaces.php:]&src/Codeception/Util/Soap.phpn:]n4uKsrc/Codeception/Util/sq.php4:]4&src/Codeception/Util/Stub.php:]x55!src/Codeception/Util/Template.php5:]5qnsrc/Codeception/Util/Uri.php,:],<src/Codeception/Util/Xml.phpz:]z~HE#src/Codeception/Util/XmlBuilder.phpH:]HtŘ%src/Codeception/Util/XmlStructure.phpZ :]Z KPText/DotReporter.php :] |hqext/Logger.phpe:]eZext/Recorder.php9T:]9T 5rext/RunBefore.php:]g 8ext/RunFailed.php :] cext/RunProcess.php :] jӶext/SimpleReporter.php:]vendor/autoload.php:]Ƕvendor/behat/gherkin/i18n.php|:]|N$˶ vendor/behat/gherkin/libpath.php:]trnǶ?vendor/behat/gherkin/src/Behat/Gherkin/Cache/CacheInterface.php :] E:vendor/behat/gherkin/src/Behat/Gherkin/Cache/FileCache.phpG:]G¶<vendor/behat/gherkin/src/Behat/Gherkin/Cache/MemoryCache.php.:].&cmCvendor/behat/gherkin/src/Behat/Gherkin/Exception/CacheException.php:]J񼩶>vendor/behat/gherkin/src/Behat/Gherkin/Exception/Exception.phpK:]KCvendor/behat/gherkin/src/Behat/Gherkin/Exception/LexerException.php:]UBvendor/behat/gherkin/src/Behat/Gherkin/Exception/NodeException.php:]tDvendor/behat/gherkin/src/Behat/Gherkin/Exception/ParserException.php:]dK?vendor/behat/gherkin/src/Behat/Gherkin/Filter/ComplexFilter.phpe:]e Hvendor/behat/gherkin/src/Behat/Gherkin/Filter/ComplexFilterInterface.php$:]$Q=϶Hvendor/behat/gherkin/src/Behat/Gherkin/Filter/FeatureFilterInterface.php:]Avendor/behat/gherkin/src/Behat/Gherkin/Filter/FilterInterface.php:]sH<vendor/behat/gherkin/src/Behat/Gherkin/Filter/LineFilter.phpM:]MБʞAvendor/behat/gherkin/src/Behat/Gherkin/Filter/LineRangeFilter.php:]rd˶<vendor/behat/gherkin/src/Behat/Gherkin/Filter/NameFilter.phpY:]Yî?Avendor/behat/gherkin/src/Behat/Gherkin/Filter/NarrativeFilter.php:][߶=vendor/behat/gherkin/src/Behat/Gherkin/Filter/PathsFilter.php:]{6ݶ<vendor/behat/gherkin/src/Behat/Gherkin/Filter/RoleFilter.phpS:]SF>vendor/behat/gherkin/src/Behat/Gherkin/Filter/SimpleFilter.php:]<;vendor/behat/gherkin/src/Behat/Gherkin/Filter/TagFilter.php:]@92vendor/behat/gherkin/src/Behat/Gherkin/Gherkin.php:]$tAvendor/behat/gherkin/src/Behat/Gherkin/Keywords/ArrayKeywords.php:]eGvendor/behat/gherkin/src/Behat/Gherkin/Keywords/CachedArrayKeywords.php:]KPDvendor/behat/gherkin/src/Behat/Gherkin/Keywords/CucumberKeywords.php:]FBvendor/behat/gherkin/src/Behat/Gherkin/Keywords/KeywordsDumper.php:]*öEvendor/behat/gherkin/src/Behat/Gherkin/Keywords/KeywordsInterface.phpZ:]Z[D0vendor/behat/gherkin/src/Behat/Gherkin/Lexer.php":]"T^ڶDvendor/behat/gherkin/src/Behat/Gherkin/Loader/AbstractFileLoader.php:]R=vendor/behat/gherkin/src/Behat/Gherkin/Loader/ArrayLoader.php:]5Avendor/behat/gherkin/src/Behat/Gherkin/Loader/DirectoryLoader.php:]ܯGEvendor/behat/gherkin/src/Behat/Gherkin/Loader/FileLoaderInterface.php:]=Cvendor/behat/gherkin/src/Behat/Gherkin/Loader/GherkinFileLoader.php:]ҶAvendor/behat/gherkin/src/Behat/Gherkin/Loader/LoaderInterface.php:]EZb@vendor/behat/gherkin/src/Behat/Gherkin/Loader/YamlFileLoader.php:]̯Avendor/behat/gherkin/src/Behat/Gherkin/Node/ArgumentInterface.phpi:]i\>vendor/behat/gherkin/src/Behat/Gherkin/Node/BackgroundNode.php:]1;vendor/behat/gherkin/src/Behat/Gherkin/Node/ExampleNode.php :] c|@vendor/behat/gherkin/src/Behat/Gherkin/Node/ExampleTableNode.phpj:]j*kI;vendor/behat/gherkin/src/Behat/Gherkin/Node/FeatureNode.php:]JCDvendor/behat/gherkin/src/Behat/Gherkin/Node/KeywordNodeInterface.php:]n=vendor/behat/gherkin/src/Behat/Gherkin/Node/NodeInterface.php:]q!z;vendor/behat/gherkin/src/Behat/Gherkin/Node/OutlineNode.phpx:]xzJL<vendor/behat/gherkin/src/Behat/Gherkin/Node/PyStringNode.php?:]?OAvendor/behat/gherkin/src/Behat/Gherkin/Node/ScenarioInterface.php:]A9pEvendor/behat/gherkin/src/Behat/Gherkin/Node/ScenarioLikeInterface.php:]x<vendor/behat/gherkin/src/Behat/Gherkin/Node/ScenarioNode.php:] BFvendor/behat/gherkin/src/Behat/Gherkin/Node/StepContainerInterface.php:]' ~8vendor/behat/gherkin/src/Behat/Gherkin/Node/StepNode.php:]Ii9vendor/behat/gherkin/src/Behat/Gherkin/Node/TableNode.php@:]@2[Cvendor/behat/gherkin/src/Behat/Gherkin/Node/TaggedNodeInterface.php:]%,1vendor/behat/gherkin/src/Behat/Gherkin/Parser.php.:].? ۶/vendor/codeception/phpunit-wrapper/RoboFile.phpP:]Pf9vendor/codeception/phpunit-wrapper/src/ConsolePrinter.php:]T=vendor/codeception/phpunit-wrapper/src/Constraint/Crawler.php:]@vendor/codeception/phpunit-wrapper/src/Constraint/CrawlerNot.php4:]4>Bvendor/codeception/phpunit-wrapper/src/Constraint/JsonContains.php:]с'>vendor/codeception/phpunit-wrapper/src/Constraint/JsonType.php:]7WY:vendor/codeception/phpunit-wrapper/src/Constraint/Page.phpO:]O3?vendor/codeception/phpunit-wrapper/src/Constraint/WebDriver.php:]4CZBvendor/codeception/phpunit-wrapper/src/Constraint/WebDriverNot.php:] 5vendor/codeception/phpunit-wrapper/src/FilterTest.php2:]2ty/vendor/codeception/phpunit-wrapper/src/Init.php:]F͕3vendor/codeception/phpunit-wrapper/src/Listener.php:]j9~4vendor/codeception/phpunit-wrapper/src/Log/JUnit.php:]6vendor/codeception/phpunit-wrapper/src/Log/PhpUnit.php :] Q*D;vendor/codeception/phpunit-wrapper/src/Overrides/Filter.phpq:]q{C¶;vendor/codeception/phpunit-wrapper/src/phpunit5-loggers.php:]u=vendor/codeception/phpunit-wrapper/src/ResultPrinter/HTML.php:]=?vendor/codeception/phpunit-wrapper/src/ResultPrinter/Report.php:]}0Lvendor/codeception/phpunit-wrapper/src/ResultPrinter/template/fail.html.dist;:];˟Pvendor/codeception/phpunit-wrapper/src/ResultPrinter/template/scenario.html.dist:]S&Qvendor/codeception/phpunit-wrapper/src/ResultPrinter/template/scenarios.html.dist:]yZ,Wvendor/codeception/phpunit-wrapper/src/ResultPrinter/template/scenario_header.html.dist3:]3ʤLvendor/codeception/phpunit-wrapper/src/ResultPrinter/template/step.html.distb:]bdPvendor/codeception/phpunit-wrapper/src/ResultPrinter/template/substeps.html.dist#:]#͠wMvendor/codeception/phpunit-wrapper/src/ResultPrinter/template/suite.html.dist3:]3|;vendor/codeception/phpunit-wrapper/src/ResultPrinter/UI.php:]IԶ8vendor/codeception/phpunit-wrapper/src/ResultPrinter.php:]9Z1vendor/codeception/phpunit-wrapper/src/Runner.php*:]*Ӷ/vendor/codeception/phpunit-wrapper/src/shim.php3:]329ﱶ3vendor/codeception/phpunit-wrapper/src/TestCase.php:]o>'vendor/codeception/specify/RoboFile.php@:]@a2=vendor/codeception/specify/src/Codeception/Specify/Config.php:]mj6{Dvendor/codeception/specify/src/Codeception/Specify/ConfigBuilder.phpW:]WEvendor/codeception/specify/src/Codeception/Specify/ObjectProperty.php;:];CY6vendor/codeception/specify/src/Codeception/Specify.phpi:]iڶ$vendor/codeception/stub/RoboFile.php:]~ $vendor/codeception/stub/src/shim.phpQ :]Q B*3vendor/codeception/stub/src/Stub/ConsecutiveMap.php:]F=K-vendor/codeception/stub/src/Stub/Expected.php;:];xq2vendor/codeception/stub/src/Stub/StubMarshaler.php:]$vendor/codeception/stub/src/Stub.php :] x1vendor/codeception/stub/src/Test/Feature/Stub.php:]k %vendor/composer/autoload_classmap.phpQS:]QSש"vendor/composer/autoload_files.php:]bdS'vendor/composer/autoload_namespaces.php:]ft!vendor/composer/autoload_psr4.php :] ?8 !vendor/composer/autoload_real.php:]״f#vendor/composer/autoload_static.php>|:]>|/նvendor/composer/ClassLoader.php:]AdWvendor/doctrine/instantiator/src/Doctrine/Instantiator/Exception/ExceptionInterface.phpj:]jζ]vendor/doctrine/instantiator/src/Doctrine/Instantiator/Exception/InvalidArgumentException.php:]p]vendor/doctrine/instantiator/src/Doctrine/Instantiator/Exception/UnexpectedValueException.php:]xiGvendor/doctrine/instantiator/src/Doctrine/Instantiator/Instantiator.php:]yGPvendor/doctrine/instantiator/src/Doctrine/Instantiator/InstantiatorInterface.php:],A¶Bvendor/facebook/webdriver/lib/AbstractWebDriverCheckboxOrRadio.php:]sr5vendor/facebook/webdriver/lib/Chrome/ChromeDriver.php#:]#~<vendor/facebook/webdriver/lib/Chrome/ChromeDriverService.php:]Bq6vendor/facebook/webdriver/lib/Chrome/ChromeOptions.phpu:]uz&%a(vendor/facebook/webdriver/lib/Cookie.php :] >>Ivendor/facebook/webdriver/lib/Exception/ElementNotSelectableException.php:]c7Fvendor/facebook/webdriver/lib/Exception/ElementNotVisibleException.php}:]}+ =vendor/facebook/webdriver/lib/Exception/ExpectedException.phpt:]tb>~ Nvendor/facebook/webdriver/lib/Exception/IMEEngineActivationFailedException.php:]SDvendor/facebook/webdriver/lib/Exception/IMENotAvailableException.php{:]{U Evendor/facebook/webdriver/lib/Exception/IndexOutOfBoundsException.php|:]|qHvendor/facebook/webdriver/lib/Exception/InvalidCookieDomainException.php:])4,ڶGvendor/facebook/webdriver/lib/Exception/InvalidCoordinatesException.php~:]~&1[WHvendor/facebook/webdriver/lib/Exception/InvalidElementStateException.php:]͕Dvendor/facebook/webdriver/lib/Exception/InvalidSelectorException.php{:]{tJvendor/facebook/webdriver/lib/Exception/MoveTargetOutOfBoundsException.php:]?ö@vendor/facebook/webdriver/lib/Exception/NoAlertOpenException.phpw:]wSzAvendor/facebook/webdriver/lib/Exception/NoCollectionException.phpx:]x*Cvendor/facebook/webdriver/lib/Exception/NoScriptResultException.phpz:]zu=vendor/facebook/webdriver/lib/Exception/NoStringException.phpt:]tC!LԶCvendor/facebook/webdriver/lib/Exception/NoStringLengthException.phpz:]zqPNDvendor/facebook/webdriver/lib/Exception/NoStringWrapperException.php{:]{f6Evendor/facebook/webdriver/lib/Exception/NoSuchCollectionException.php|:]|g퉶Cvendor/facebook/webdriver/lib/Exception/NoSuchDocumentException.phpz:]z`_ƶAvendor/facebook/webdriver/lib/Exception/NoSuchDriverException.phpx:]x4LJBvendor/facebook/webdriver/lib/Exception/NoSuchElementException.phpy:]yq@vendor/facebook/webdriver/lib/Exception/NoSuchFrameException.phpw:]wWbdAvendor/facebook/webdriver/lib/Exception/NoSuchWindowException.phpx:]x,l]@vendor/facebook/webdriver/lib/Exception/NullPointerException.phpw:]wBvendor/facebook/webdriver/lib/Exception/ScriptTimeoutException.phpy:]y 57,Fvendor/facebook/webdriver/lib/Exception/SessionNotCreatedException.php}:]}$lJvendor/facebook/webdriver/lib/Exception/StaleElementReferenceException.php:]+<vendor/facebook/webdriver/lib/Exception/TimeOutException.phps:]sQ+]3Fvendor/facebook/webdriver/lib/Exception/UnableToSetCookieException.php}:]}^Hvendor/facebook/webdriver/lib/Exception/UnexpectedAlertOpenException.php:]RIvendor/facebook/webdriver/lib/Exception/UnexpectedJavascriptException.php:]@xӶFvendor/facebook/webdriver/lib/Exception/UnexpectedTagNameException.php?:]?P̶Cvendor/facebook/webdriver/lib/Exception/UnknownCommandException.phpz:]zN(xBvendor/facebook/webdriver/lib/Exception/UnknownServerException.phpy:]yZJvendor/facebook/webdriver/lib/Exception/UnrecognizedExceptionException.php:]LģIvendor/facebook/webdriver/lib/Exception/UnsupportedOperationException.php:]xVBvendor/facebook/webdriver/lib/Exception/WebDriverCurlException.phpy:]y'>vendor/facebook/webdriver/lib/Exception/WebDriverException.php :] Uı@vendor/facebook/webdriver/lib/Exception/XPathLookupException.phpw:]wͯ7vendor/facebook/webdriver/lib/Firefox/FirefoxDriver.php:]]<vendor/facebook/webdriver/lib/Firefox/FirefoxPreferences.php@:]@ڭ8vendor/facebook/webdriver/lib/Firefox/FirefoxProfile.phpW:]W¬/uTvendor/facebook/webdriver/lib/Interactions/Internal/WebDriverButtonReleaseAction.php%:]%_kfLvendor/facebook/webdriver/lib/Interactions/Internal/WebDriverClickAction.php:]SSvendor/facebook/webdriver/lib/Interactions/Internal/WebDriverClickAndHoldAction.php&:]& )ؼSvendor/facebook/webdriver/lib/Interactions/Internal/WebDriverContextClickAction.php):])4ӶLvendor/facebook/webdriver/lib/Interactions/Internal/WebDriverCoordinates.phpn:]n,k}Rvendor/facebook/webdriver/lib/Interactions/Internal/WebDriverDoubleClickAction.php$:]$P.$Nvendor/facebook/webdriver/lib/Interactions/Internal/WebDriverKeyDownAction.php-:]-׌Rvendor/facebook/webdriver/lib/Interactions/Internal/WebDriverKeysRelatedAction.php:]3(VLvendor/facebook/webdriver/lib/Interactions/Internal/WebDriverKeyUpAction.php-:]-ÊLvendor/facebook/webdriver/lib/Interactions/Internal/WebDriverMouseAction.php:]69Pvendor/facebook/webdriver/lib/Interactions/Internal/WebDriverMouseMoveAction.php :] Y~ҶSvendor/facebook/webdriver/lib/Interactions/Internal/WebDriverMoveToOffsetAction.php:] Ovendor/facebook/webdriver/lib/Interactions/Internal/WebDriverSendKeysAction.php:]8Pvendor/facebook/webdriver/lib/Interactions/Internal/WebDriverSingleKeyAction.phpV:]V(qMvendor/facebook/webdriver/lib/Interactions/Touch/WebDriverDoubleTapAction.php :]  PHvendor/facebook/webdriver/lib/Interactions/Touch/WebDriverDownAction.php:]pD}Ivendor/facebook/webdriver/lib/Interactions/Touch/WebDriverFlickAction.php:]\,gTvendor/facebook/webdriver/lib/Interactions/Touch/WebDriverFlickFromElementAction.php:]-ĶMvendor/facebook/webdriver/lib/Interactions/Touch/WebDriverLongPressAction.php :] `Hvendor/facebook/webdriver/lib/Interactions/Touch/WebDriverMoveAction.php:]8Jvendor/facebook/webdriver/lib/Interactions/Touch/WebDriverScrollAction.php:]΍3Uvendor/facebook/webdriver/lib/Interactions/Touch/WebDriverScrollFromElementAction.phpD:]DeGvendor/facebook/webdriver/lib/Interactions/Touch/WebDriverTapAction.php:]_yC+Ivendor/facebook/webdriver/lib/Interactions/Touch/WebDriverTouchAction.phpz:]zjɶIvendor/facebook/webdriver/lib/Interactions/Touch/WebDriverTouchScreen.php:]ޘж?vendor/facebook/webdriver/lib/Interactions/WebDriverActions.php<:]<A~ӶGvendor/facebook/webdriver/lib/Interactions/WebDriverCompositeAction.php:]* Dvendor/facebook/webdriver/lib/Interactions/WebDriverTouchActions.phpL :]L =vendor/facebook/webdriver/lib/Internal/WebDriverLocatable.php:]b4vendor/facebook/webdriver/lib/JavaScriptExecutor.php:]M;d0vendor/facebook/webdriver/lib/Net/URLChecker.php}:]}&S<vendor/facebook/webdriver/lib/Remote/DesiredCapabilities.php$:]$X 6vendor/facebook/webdriver/lib/Remote/DriverCommand.php:]縶6vendor/facebook/webdriver/lib/Remote/ExecuteMethod.php:]P5vendor/facebook/webdriver/lib/Remote/FileDetector.php:]ؘ<vendor/facebook/webdriver/lib/Remote/HttpCommandExecutor.php.:].FZ:vendor/facebook/webdriver/lib/Remote/LocalFileDetector.php:]L<vendor/facebook/webdriver/lib/Remote/RemoteExecuteMethod.php_:]_}^7vendor/facebook/webdriver/lib/Remote/RemoteKeyboard.php+:]+l4vendor/facebook/webdriver/lib/Remote/RemoteMouse.php:]]̛<vendor/facebook/webdriver/lib/Remote/RemoteTargetLocator.phpK:]K#o:vendor/facebook/webdriver/lib/Remote/RemoteTouchScreen.phpG:]G#8vendor/facebook/webdriver/lib/Remote/RemoteWebDriver.phpq!:]q!G{)9vendor/facebook/webdriver/lib/Remote/RemoteWebElement.php:]NFvendor/facebook/webdriver/lib/Remote/Service/DriverCommandExecutor.php:]a>vendor/facebook/webdriver/lib/Remote/Service/DriverService.php:]5<vendor/facebook/webdriver/lib/Remote/UselessFileDetector.php:]f#=vendor/facebook/webdriver/lib/Remote/WebDriverBrowserType.php:]e @vendor/facebook/webdriver/lib/Remote/WebDriverCapabilityType.php5:]5"Lh9vendor/facebook/webdriver/lib/Remote/WebDriverCommand.php:]@_}:vendor/facebook/webdriver/lib/Remote/WebDriverResponse.php:]|Evendor/facebook/webdriver/lib/Support/Events/EventFiringWebDriver.php:]'Ovendor/facebook/webdriver/lib/Support/Events/EventFiringWebDriverNavigation.php.:].l Fvendor/facebook/webdriver/lib/Support/Events/EventFiringWebElement.php:]x6vendor/facebook/webdriver/lib/Support/XPathEscaper.php:]8+vendor/facebook/webdriver/lib/WebDriver.php:]t4ܶ1vendor/facebook/webdriver/lib/WebDriverAction.phpo:]ot.z0vendor/facebook/webdriver/lib/WebDriverAlert.php:]TŶ-vendor/facebook/webdriver/lib/WebDriverBy.phph:]hJh7vendor/facebook/webdriver/lib/WebDriverCapabilities.phpN:]NF5vendor/facebook/webdriver/lib/WebDriverCheckboxes.php:]2}P:vendor/facebook/webdriver/lib/WebDriverCommandExecutor.php:]#4vendor/facebook/webdriver/lib/WebDriverDimension.php:]Ae25vendor/facebook/webdriver/lib/WebDriverDispatcher.phpX:]X7|2vendor/facebook/webdriver/lib/WebDriverElement.php:]z8vendor/facebook/webdriver/lib/WebDriverEventListener.php:]u{<vendor/facebook/webdriver/lib/WebDriverExpectedCondition.php:]9e:vendor/facebook/webdriver/lib/WebDriverHasInputDevices.php:]uJ3vendor/facebook/webdriver/lib/WebDriverKeyboard.php:]b/vendor/facebook/webdriver/lib/WebDriverKeys.php[ :][ 0vendor/facebook/webdriver/lib/WebDriverMouse.php+:]+x?5vendor/facebook/webdriver/lib/WebDriverNavigation.php:] 7[2vendor/facebook/webdriver/lib/WebDriverOptions.php:]3vendor/facebook/webdriver/lib/WebDriverPlatform.php(:](T:Զ0vendor/facebook/webdriver/lib/WebDriverPoint.php`:]`Odž1vendor/facebook/webdriver/lib/WebDriverRadios.php{:]{1j8vendor/facebook/webdriver/lib/WebDriverSearchContext.php:] 툶1vendor/facebook/webdriver/lib/WebDriverSelect.php:]h/:vendor/facebook/webdriver/lib/WebDriverSelectInterface.phpM:]Mjn8vendor/facebook/webdriver/lib/WebDriverTargetLocator.php:]D3vendor/facebook/webdriver/lib/WebDriverTimeouts.php:];0`3vendor/facebook/webdriver/lib/WebDriverUpAction.php:]Z/vendor/facebook/webdriver/lib/WebDriverWait.phpH:]H31vendor/facebook/webdriver/lib/WebDriverWindow.phpj:]jJ<<7vendor/flow/jsonpath/src/Flow/JSONPath/AccessHelper.php :] 2Avendor/flow/jsonpath/src/Flow/JSONPath/Filters/AbstractFilter.php#:]# >@vendor/flow/jsonpath/src/Flow/JSONPath/Filters/IndexesFilter.php:]Ծ>vendor/flow/jsonpath/src/Flow/JSONPath/Filters/IndexFilter.php:]jCvendor/flow/jsonpath/src/Flow/JSONPath/Filters/QueryMatchFilter.phpF:]FɂDvendor/flow/jsonpath/src/Flow/JSONPath/Filters/QueryResultFilter.php:] ٶBvendor/flow/jsonpath/src/Flow/JSONPath/Filters/RecursiveFilter.php,:],`]>vendor/flow/jsonpath/src/Flow/JSONPath/Filters/SliceFilter.php:]3vendor/flow/jsonpath/src/Flow/JSONPath/JSONPath.php :] =¶<vendor/flow/jsonpath/src/Flow/JSONPath/JSONPathException.php_:]_ms>l8vendor/flow/jsonpath/src/Flow/JSONPath/JSONPathLexer.php+:]+8D׶8vendor/flow/jsonpath/src/Flow/JSONPath/JSONPathToken.php:]r 'vendor/guzzlehttp/guzzle/src/Client.php!:]!a0vendor/guzzlehttp/guzzle/src/ClientInterface.php:]G 1vendor/guzzlehttp/guzzle/src/Cookie/CookieJar.php:]̥:vendor/guzzlehttp/guzzle/src/Cookie/CookieJarInterface.phpH:]H?I5vendor/guzzlehttp/guzzle/src/Cookie/FileCookieJar.php:]ζ8vendor/guzzlehttp/guzzle/src/Cookie/SessionCookieJar.php:]ms1vendor/guzzlehttp/guzzle/src/Cookie/SetCookie.php:]-O }?vendor/guzzlehttp/guzzle/src/Exception/BadResponseException.phpc:]c)k-:vendor/guzzlehttp/guzzle/src/Exception/ClientException.php`:]`p;vendor/guzzlehttp/guzzle/src/Exception/ConnectException.php:]հ:vendor/guzzlehttp/guzzle/src/Exception/GuzzleException.phpM:]MkT;vendor/guzzlehttp/guzzle/src/Exception/RequestException.php :] 2!S8vendor/guzzlehttp/guzzle/src/Exception/SeekException.php:]ZM%]:vendor/guzzlehttp/guzzle/src/Exception/ServerException.php`:]` Dvendor/guzzlehttp/guzzle/src/Exception/TooManyRedirectsException.phpc:]c߶<vendor/guzzlehttp/guzzle/src/Exception/TransferException.phpw:]w Q*vendor/guzzlehttp/guzzle/src/functions.phpJ:]Jo2vendor/guzzlehttp/guzzle/src/functions_include.phpa:]aj4vendor/guzzlehttp/guzzle/src/Handler/CurlFactory.php/:]/=vendor/guzzlehttp/guzzle/src/Handler/CurlFactoryInterface.php:]u4vendor/guzzlehttp/guzzle/src/Handler/CurlHandler.phpv:]v9f9vendor/guzzlehttp/guzzle/src/Handler/CurlMultiHandler.php :] z.W3vendor/guzzlehttp/guzzle/src/Handler/EasyHandle.php9:]9:\4vendor/guzzlehttp/guzzle/src/Handler/MockHandler.phpa :]a U~.vendor/guzzlehttp/guzzle/src/Handler/Proxy.php:]˶6vendor/guzzlehttp/guzzle/src/Handler/StreamHandler.php4-:]4-RM-vendor/guzzlehttp/guzzle/src/HandlerStack.php :] a1vendor/guzzlehttp/guzzle/src/MessageFormatter.php :] +vendor/guzzlehttp/guzzle/src/Middleware.php:]9gl%vendor/guzzlehttp/guzzle/src/Pool.phpw:]w6vendor/guzzlehttp/guzzle/src/PrepareBodyMiddleware.php:]/v3vendor/guzzlehttp/guzzle/src/RedirectMiddleware.php:]H/vendor/guzzlehttp/guzzle/src/RequestOptions.phpc:]c.{\0vendor/guzzlehttp/guzzle/src/RetryMiddleware.php:]}SU.vendor/guzzlehttp/guzzle/src/TransferStats.php:]/c,vendor/guzzlehttp/guzzle/src/UriTemplate.php]:]]&5vendor/guzzlehttp/promises/src/AggregateException.php:]O]8vendor/guzzlehttp/promises/src/CancellationException.phpc:]c!0,vendor/guzzlehttp/promises/src/Coroutine.php:]r3&.vendor/guzzlehttp/promises/src/EachPromise.php :] -3vendor/guzzlehttp/promises/src/FulfilledPromise.php:]D,vendor/guzzlehttp/promises/src/functions.php:]+ 4vendor/guzzlehttp/promises/src/functions_include.phph:]hʏ*vendor/guzzlehttp/promises/src/Promise.php:]ڻS3vendor/guzzlehttp/promises/src/PromiseInterface.php:]sö4vendor/guzzlehttp/promises/src/PromisorInterface.phph:]hi2vendor/guzzlehttp/promises/src/RejectedPromise.php:]]5vendor/guzzlehttp/promises/src/RejectionException.php:]3,vendor/guzzlehttp/promises/src/TaskQueue.php:]C]5vendor/guzzlehttp/promises/src/TaskQueueInterface.php:]j{+vendor/guzzlehttp/psr7/src/AppendStream.php :] 颶+vendor/guzzlehttp/psr7/src/BufferStream.php):])1,vendor/guzzlehttp/psr7/src/CachingStream.phpx:]xà-vendor/guzzlehttp/psr7/src/DroppingStream.php/:]/FE;'vendor/guzzlehttp/psr7/src/FnStream.php:]!Ҍ(vendor/guzzlehttp/psr7/src/functions.php::]:ֶ0vendor/guzzlehttp/psr7/src/functions_include.php]:]]ɶ,vendor/guzzlehttp/psr7/src/InflateStream.php:]TL-vendor/guzzlehttp/psr7/src/LazyOpenStream.php:]bE*vendor/guzzlehttp/psr7/src/LimitStream.php:]0+vendor/guzzlehttp/psr7/src/MessageTrait.phpk :]k i~.vendor/guzzlehttp/psr7/src/MultipartStream.phpH :]H +vendor/guzzlehttp/psr7/src/NoSeekStream.php::]:R!)vendor/guzzlehttp/psr7/src/PumpStream.php_:]_!IU&vendor/guzzlehttp/psr7/src/Request.php]:]]K'vendor/guzzlehttp/psr7/src/Response.php, :], mƶ&vendor/guzzlehttp/psr7/src/Rfc7230.php:],vendor/guzzlehttp/psr7/src/ServerRequest.php":]"n,㬶%vendor/guzzlehttp/psr7/src/Stream.php:]B=3vendor/guzzlehttp/psr7/src/StreamDecoratorTrait.php8:]8b,vendor/guzzlehttp/psr7/src/StreamWrapper.php:]=A+vendor/guzzlehttp/psr7/src/UploadedFile.php:]>#"vendor/guzzlehttp/psr7/src/Uri.phpH&:]H&ɹ,vendor/guzzlehttp/psr7/src/UriNormalizer.php :] 2*vendor/guzzlehttp/psr7/src/UriResolver.php:]2Ŷ%vendor/hoa/consistency/Autoloader.php :] LC&vendor/hoa/consistency/Consistency.phpy:]y͑ж$vendor/hoa/consistency/Exception.php:]["vendor/hoa/consistency/Prelude.php:]/vendor/hoa/consistency/Test/Unit/Autoloader.phpf:]fw?0vendor/hoa/consistency/Test/Unit/Consistency.php:]UT.vendor/hoa/consistency/Test/Unit/Exception.phpO:]OD1K.vendor/hoa/consistency/Test/Unit/Xcallable.php:]$$vendor/hoa/consistency/Xcallable.php :] "vendor/hoa/console/Bin/Termcap.php? :]? ߶$vendor/hoa/console/Chrome/Editor.php:]G)'vendor/hoa/console/Chrome/Exception.php:]`D!#vendor/hoa/console/Chrome/Pager.php_:]_"vendor/hoa/console/Chrome/Text.php:]1{vendor/hoa/console/Console.php :] nvendor/hoa/console/Cursor.php&#:]&#(WҶ%vendor/hoa/console/Dispatcher/Kit.php :] kh vendor/hoa/console/Exception.php:]q$| vendor/hoa/console/GetOption.php:]ݶvendor/hoa/console/Input.php:]Zvendor/hoa/console/Mouse.php :] 4Uvendor/hoa/console/Output.php{:]{t3vendor/hoa/console/Parser.php :] LC vendor/hoa/console/Processus.phpY(:]Y(ڶ7vendor/hoa/console/Readline/Autocompleter/Aggregate.php:]ᤶ;vendor/hoa/console/Readline/Autocompleter/Autocompleter.php::]:e2vendor/hoa/console/Readline/Autocompleter/Path.php:]>2vendor/hoa/console/Readline/Autocompleter/Word.php:]K̶(vendor/hoa/console/Readline/Password.php:]0ڶ(vendor/hoa/console/Readline/Readline.php;:];uGɶ(vendor/hoa/console/Test/Unit/Console.php :] EN<'vendor/hoa/console/Test/Unit/Cursor.phpE4:]E40e*vendor/hoa/console/Test/Unit/GetOption.php_ :]_ sB&vendor/hoa/console/Test/Unit/Input.php|:]|L@&vendor/hoa/console/Test/Unit/Mouse.php :] x'vendor/hoa/console/Test/Unit/Output.php :] ΝM$'vendor/hoa/console/Test/Unit/Parser.php:]dAvendor/hoa/console/Test/Unit/Readline/Autocompleter/Aggregate.phpx :]x <vendor/hoa/console/Test/Unit/Readline/Autocompleter/Path.php{:]{{<vendor/hoa/console/Test/Unit/Readline/Autocompleter/Word.php~:]~sܕ2vendor/hoa/console/Test/Unit/Readline/Password.php:]e0%vendor/hoa/console/Test/Unit/Tput.php":]"h#'vendor/hoa/console/Test/Unit/Window.php:]h{vendor/hoa/console/Tput.php/:]/bvendor/hoa/console/Window.phpv:]v2ivendor/hoa/event/Bucket.php:]vvendor/hoa/event/Event.php :] x`cvendor/hoa/event/Exception.php:]x샶vendor/hoa/event/Listenable.php:] vendor/hoa/event/Listener.php:]*>vendor/hoa/event/Listens.php~:]~wvendor/hoa/event/Source.php\:]\%vendor/hoa/event/Test/Unit/Bucket.php:]^$vendor/hoa/event/Test/Unit/Event.phpH:]H{._ֶ(vendor/hoa/event/Test/Unit/Exception.phpC:]Cg%L)vendor/hoa/event/Test/Unit/Listenable.php,:],'5'vendor/hoa/event/Test/Unit/Listener.php:]1&vendor/hoa/event/Test/Unit/Listens.php:]*]o%vendor/hoa/event/Test/Unit/Source.php :] VbJvendor/hoa/exception/Error.php:]M"vendor/hoa/exception/Exception.php:]Gvendor/hoa/exception/Group.phpH :]H Avendor/hoa/exception/Idle.php :] (vendor/hoa/exception/Test/Unit/Error.php:]ݶ,vendor/hoa/exception/Test/Unit/Exception.php:]A(vendor/hoa/exception/Test/Unit/Group.php+:]+Mc'vendor/hoa/exception/Test/Unit/Idle.php:]vendor/hoa/file/Directory.php :] a'vendor/hoa/file/Exception/Exception.php:](.vendor/hoa/file/Exception/FileDoesNotExist.php}:]}c@vendor/hoa/file/File.phpx :]x /kvendor/hoa/file/Finder.php:]ţvendor/hoa/file/Generic.phpj:]j1ƶvendor/hoa/file/Link/Link.php:]ֶvendor/hoa/file/Link/Read.php:]F'"vendor/hoa/file/Link/ReadWrite.php :] (#vendor/hoa/file/Link/Write.php* :]* vendor/hoa/file/Read.php:]W)Jvendor/hoa/file/ReadWrite.phpa :]a 8vendor/hoa/file/SplFileInfo.php:]H+"vendor/hoa/file/Temporary/Read.php:]Cv.'vendor/hoa/file/Temporary/ReadWrite.php :] R'vendor/hoa/file/Temporary/Temporary.php:]_芶#vendor/hoa/file/Temporary/Write.php5 :]5 q vendor/hoa/file/Watcher.phpI:]I':vendor/hoa/file/Write.php< :]< S!vendor/hoa/iterator/Aggregate.php}:]}u6vendor/hoa/iterator/Append.phps:]s?3vendor/hoa/iterator/Buffer.php:]%&vendor/hoa/iterator/CallbackFilter.php:]lf)vendor/hoa/iterator/CallbackGenerator.php:] Զvendor/hoa/iterator/Counter.php4:]4%vendor/hoa/iterator/Demultiplexer.php:]v!vendor/hoa/iterator/Directory.php:]̇vж!vendor/hoa/iterator/Exception.php:]Isq"vendor/hoa/iterator/FileSystem.php~:]~^vendor/hoa/iterator/Filter.php|:]|VXvendor/hoa/iterator/Glob.phpo:]oS{ vendor/hoa/iterator/Infinite.phpw:]wN vendor/hoa/iterator/Iterator.php:]`(vendor/hoa/iterator/IteratorIterator.php:]tvendor/hoa/iterator/Limit.phpq:]q!vendor/hoa/iterator/Lookahead.php:]H'="vendor/hoa/iterator/Lookbehind.php:]JE|vendor/hoa/iterator/Map.phpo:]o]նvendor/hoa/iterator/Mock.phpp:]pZW{ vendor/hoa/iterator/Multiple.phpF:]Fg2 vendor/hoa/iterator/NoRewind.phpw:]wIfvendor/hoa/iterator/Outer.phpu:]u/0vendor/hoa/iterator/Recursive/CallbackFilter.php:]Ń+vendor/hoa/iterator/Recursive/Directory.php:]el¶(vendor/hoa/iterator/Recursive/Filter.php:]o*vendor/hoa/iterator/Recursive/Iterator.php:]Ҷ%vendor/hoa/iterator/Recursive/Map.php:]*\&vendor/hoa/iterator/Recursive/Mock.php:]>j+vendor/hoa/iterator/Recursive/Recursive.php:]3vendor/hoa/iterator/Recursive/RegularExpression.php/:]/ e3)vendor/hoa/iterator/RegularExpression.php@ :]@ 댶 vendor/hoa/iterator/Repeater.php`:]`1vendor/hoa/stream/Test/Unit/Wrapper/Exception.phpg:]gO5vendor/hoa/stream/Test/Unit/Wrapper/IWrapper/File.phpp:]pj_9vendor/hoa/stream/Test/Unit/Wrapper/IWrapper/IWrapper.php:])W7vendor/hoa/stream/Test/Unit/Wrapper/IWrapper/Stream.phpv:]v ڛ/vendor/hoa/stream/Test/Unit/Wrapper/Wrapper.php :] W&'vendor/hoa/stream/Wrapper/Exception.php:]}%m+vendor/hoa/stream/Wrapper/IWrapper/File.php:]+Ķ/vendor/hoa/stream/Wrapper/IWrapper/IWrapper.php:]wzY-vendor/hoa/stream/Wrapper/IWrapper/Stream.phpr:]r_2%vendor/hoa/stream/Wrapper/Wrapper.php:]lD#vendor/hoa/ustring/Bin/Fromcode.php:] s !vendor/hoa/ustring/Bin/Tocode.php:]xHӶ vendor/hoa/ustring/Exception.php:]Hvendor/hoa/ustring/Search.php:]OIV&vendor/hoa/ustring/Test/Unit/Issue.php?:]?h'vendor/hoa/ustring/Test/Unit/Search.php:]Ԡݶ(vendor/hoa/ustring/Test/Unit/Ustring.phpD:]Dvendor/hoa/ustring/Ustring.php.:].42[C&vendor/myclabs/deep-copy/doc/clone.png\0:]\0+vendor/myclabs/deep-copy/doc/deep-clone.png6:]6JŨ*vendor/myclabs/deep-copy/doc/deep-copy.png*:]*i9&vendor/myclabs/deep-copy/doc/graph.png$:]$P϶,vendor/myclabs/deep-copy/fixtures/f001/A.php:](Q,vendor/myclabs/deep-copy/fixtures/f001/B.php:]+,vendor/myclabs/deep-copy/fixtures/f002/A.phpK:]K.vendor/myclabs/deep-copy/fixtures/f003/Foo.php:]9vendor/myclabs/deep-copy/fixtures/f004/UnclonableItem.php:]BUy.vendor/myclabs/deep-copy/fixtures/f005/Foo.php|:]| 3Zq,vendor/myclabs/deep-copy/fixtures/f006/A.php :] |;,vendor/myclabs/deep-copy/fixtures/f006/B.php :] <3:vendor/myclabs/deep-copy/fixtures/f007/FooDateInterval.php:]G/:vendor/myclabs/deep-copy/fixtures/f007/FooDateTimeZone.php:] ,vendor/myclabs/deep-copy/fixtures/f008/A.php:]vendor/myclabs/deep-copy/src/DeepCopy/Filter/ReplaceFilter.php":]"G>vendor/myclabs/deep-copy/src/DeepCopy/Filter/SetNullFilter.php_:]_|GضOvendor/myclabs/deep-copy/src/DeepCopy/Matcher/Doctrine/DoctrineProxyMatcher.php:]SӶ9vendor/myclabs/deep-copy/src/DeepCopy/Matcher/Matcher.phpm:]ma˶Avendor/myclabs/deep-copy/src/DeepCopy/Matcher/PropertyMatcher.phpj:]jc$CEvendor/myclabs/deep-copy/src/DeepCopy/Matcher/PropertyNameMatcher.php:]kEvendor/myclabs/deep-copy/src/DeepCopy/Matcher/PropertyTypeMatcher.phpK:]KV~zEvendor/myclabs/deep-copy/src/DeepCopy/Reflection/ReflectionHelper.php:]Lvendor/myclabs/deep-copy/src/DeepCopy/TypeFilter/Date/DateIntervalFilter.php[:][%MQBvendor/myclabs/deep-copy/src/DeepCopy/TypeFilter/ReplaceFilter.php:]›qFvendor/myclabs/deep-copy/src/DeepCopy/TypeFilter/ShallowCopyFilter.php:]gLvendor/myclabs/deep-copy/src/DeepCopy/TypeFilter/Spl/SplDoublyLinkedList.phpn:]nRvendor/myclabs/deep-copy/src/DeepCopy/TypeFilter/Spl/SplDoublyLinkedListFilter.php:]&)y?vendor/myclabs/deep-copy/src/DeepCopy/TypeFilter/TypeFilter.phpf:]fZ.)ȶAvendor/myclabs/deep-copy/src/DeepCopy/TypeMatcher/TypeMatcher.php:]466vendor/phpdocumentor/reflection-common/src/Element.php:]#3vendor/phpdocumentor/reflection-common/src/File.php:]6޶4vendor/phpdocumentor/reflection-common/src/Fqsen.php:]ՙ7vendor/phpdocumentor/reflection-common/src/Location.php:]@6vendor/phpdocumentor/reflection-common/src/Project.phpn:]nj=vendor/phpdocumentor/reflection-common/src/ProjectFactory.php:]{Evendor/phpdocumentor/reflection-docblock/src/DocBlock/Description.phpi:]i9Lvendor/phpdocumentor/reflection-docblock/src/DocBlock/DescriptionFactory.php :] ^ Gvendor/phpdocumentor/reflection-docblock/src/DocBlock/ExampleFinder.phpL:]L*dDvendor/phpdocumentor/reflection-docblock/src/DocBlock/Serializer.php :] JlLvendor/phpdocumentor/reflection-docblock/src/DocBlock/StandardTagFactory.php:])Ś=vendor/phpdocumentor/reflection-docblock/src/DocBlock/Tag.php:]KxDvendor/phpdocumentor/reflection-docblock/src/DocBlock/TagFactory.php:]fdEvendor/phpdocumentor/reflection-docblock/src/DocBlock/Tags/Author.php:]Fvendor/phpdocumentor/reflection-docblock/src/DocBlock/Tags/BaseTag.php+:]+G9Evendor/phpdocumentor/reflection-docblock/src/DocBlock/Tags/Covers.php:]Ivendor/phpdocumentor/reflection-docblock/src/DocBlock/Tags/Deprecated.php:]Fvendor/phpdocumentor/reflection-docblock/src/DocBlock/Tags/Example.php :] <*Svendor/phpdocumentor/reflection-docblock/src/DocBlock/Tags/Factory/StaticMethod.php:]F Ovendor/phpdocumentor/reflection-docblock/src/DocBlock/Tags/Factory/Strategy.php:]joWvendor/phpdocumentor/reflection-docblock/src/DocBlock/Tags/Formatter/AlignFormatter.php:]]vendor/phpdocumentor/reflection-docblock/src/DocBlock/Tags/Formatter/PassthroughFormatter.phpI:]I߽䞶Hvendor/phpdocumentor/reflection-docblock/src/DocBlock/Tags/Formatter.php:]1DFvendor/phpdocumentor/reflection-docblock/src/DocBlock/Tags/Generic.php8:]8ѶCvendor/phpdocumentor/reflection-docblock/src/DocBlock/Tags/Link.php>:]>JEvendor/phpdocumentor/reflection-docblock/src/DocBlock/Tags/Method.php:]Dvendor/phpdocumentor/reflection-docblock/src/DocBlock/Tags/Param.php:]"ѶGvendor/phpdocumentor/reflection-docblock/src/DocBlock/Tags/Property.phpj:]j{Kvendor/phpdocumentor/reflection-docblock/src/DocBlock/Tags/PropertyRead.phps:]sF`%CLvendor/phpdocumentor/reflection-docblock/src/DocBlock/Tags/PropertyWrite.phpu:]uxNvendor/phpdocumentor/reflection-docblock/src/DocBlock/Tags/Reference/Fqsen.phpP:]PRvendor/phpdocumentor/reflection-docblock/src/DocBlock/Tags/Reference/Reference.php:]uLvendor/phpdocumentor/reflection-docblock/src/DocBlock/Tags/Reference/Url.php9:]9tLFvendor/phpdocumentor/reflection-docblock/src/DocBlock/Tags/Return_.php:]eBvendor/phpdocumentor/reflection-docblock/src/DocBlock/Tags/See.php:]Dvendor/phpdocumentor/reflection-docblock/src/DocBlock/Tags/Since.phpA:]AP4GEvendor/phpdocumentor/reflection-docblock/src/DocBlock/Tags/Source.phpm:]mWEvendor/phpdocumentor/reflection-docblock/src/DocBlock/Tags/Throws.php:]`Cvendor/phpdocumentor/reflection-docblock/src/DocBlock/Tags/Uses.php:]Cvendor/phpdocumentor/reflection-docblock/src/DocBlock/Tags/Var_.php:]rmFvendor/phpdocumentor/reflection-docblock/src/DocBlock/Tags/Version.phpD:]DܙZ9vendor/phpdocumentor/reflection-docblock/src/DocBlock.php:]o@<@vendor/phpdocumentor/reflection-docblock/src/DocBlockFactory.php!:]!h~o Ivendor/phpdocumentor/reflection-docblock/src/DocBlockFactoryInterface.php:]]F 8vendor/phpdocumentor/type-resolver/src/FqsenResolver.php3:]3ʩ¶/vendor/phpdocumentor/type-resolver/src/Type.phpg:]gGF:7vendor/phpdocumentor/type-resolver/src/TypeResolver.php%:]%~di7vendor/phpdocumentor/type-resolver/src/Types/Array_.php:]8vendor/phpdocumentor/type-resolver/src/Types/Boolean.php:]7:vendor/phpdocumentor/type-resolver/src/Types/Callable_.php:]%G{9vendor/phpdocumentor/type-resolver/src/Types/Compound.php:]֟8vendor/phpdocumentor/type-resolver/src/Types/Context.php:]K?vendor/phpdocumentor/type-resolver/src/Types/ContextFactory.php; :]; `7vendor/phpdocumentor/type-resolver/src/Types/Float_.php:]W8vendor/phpdocumentor/type-resolver/src/Types/Integer.php:]t`9:vendor/phpdocumentor/type-resolver/src/Types/Iterable_.php:]<7vendor/phpdocumentor/type-resolver/src/Types/Mixed_.php:]"9vendor/phpdocumentor/type-resolver/src/Types/Nullable.php:]`6vendor/phpdocumentor/type-resolver/src/Types/Null_.php:]J8vendor/phpdocumentor/type-resolver/src/Types/Object_.php:] 8vendor/phpdocumentor/type-resolver/src/Types/Parent_.php:]e:vendor/phpdocumentor/type-resolver/src/Types/Resource_.php:]-7vendor/phpdocumentor/type-resolver/src/Types/Scalar.php:]D,66vendor/phpdocumentor/type-resolver/src/Types/Self_.php:]{Ķ8vendor/phpdocumentor/type-resolver/src/Types/Static_.php:]A58vendor/phpdocumentor/type-resolver/src/Types/String_.php:]w.5vendor/phpdocumentor/type-resolver/src/Types/This.php:]V56vendor/phpdocumentor/type-resolver/src/Types/Void_.php:]%2۶7vendor/phpoption/phpoption/src/PhpOption/LazyOption.php:]Ʉ1vendor/phpoption/phpoption/src/PhpOption/None.php<:]<ڍ3vendor/phpoption/phpoption/src/PhpOption/Option.phpv:]v 11vendor/phpoption/phpoption/src/PhpOption/Some.php:]WҐ5vendor/phpunit/php-code-coverage/src/CodeCoverage.php?:]?Į6vendor/phpunit/php-code-coverage/src/Driver/Driver.php:]+\4vendor/phpunit/php-code-coverage/src/Driver/HHVM.php:] e6vendor/phpunit/php-code-coverage/src/Driver/PHPDBG.php:]%6vendor/phpunit/php-code-coverage/src/Driver/Xdebug.php:]H(#Rvendor/phpunit/php-code-coverage/src/Exception/CoveredCodeNotExecutedException.php:]Sō<vendor/phpunit/php-code-coverage/src/Exception/Exception.phpU:]USw^Kvendor/phpunit/php-code-coverage/src/Exception/InvalidArgumentException.php:](Svendor/phpunit/php-code-coverage/src/Exception/MissingCoversAnnotationException.php:]lՔCvendor/phpunit/php-code-coverage/src/Exception/RuntimeException.php:]KVvendor/phpunit/php-code-coverage/src/Exception/UnintentionallyCoveredCodeException.phpk:]k A/vendor/phpunit/php-code-coverage/src/Filter.php:]<37:vendor/phpunit/php-code-coverage/src/Node/AbstractNode.php :] XA5vendor/phpunit/php-code-coverage/src/Node/Builder.php :] N'7vendor/phpunit/php-code-coverage/src/Node/Directory.php:]K2vendor/phpunit/php-code-coverage/src/Node/File.php):]) 6vendor/phpunit/php-code-coverage/src/Node/Iterator.php:]΄Б6vendor/phpunit/php-code-coverage/src/Report/Clover.php:]E6vendor/phpunit/php-code-coverage/src/Report/Crap4j.phpw:]wo;vendor/phpunit/php-code-coverage/src/Report/Html/Facade.php]:]]BGvendor/phpunit/php-code-coverage/src/Report/Html/Renderer/Dashboard.php:]FGvendor/phpunit/php-code-coverage/src/Report/Html/Renderer/Directory.php2:]2 ‚Bvendor/phpunit/php-code-coverage/src/Report/Html/Renderer/File.phpC%:]C%@BYvendor/phpunit/php-code-coverage/src/Report/Html/Renderer/Template/coverage_bar.html.dist1:]1itLXvendor/phpunit/php-code-coverage/src/Report/Html/Renderer/Template/css/bootstrap.min.cssp:]pX|FTvendor/phpunit/php-code-coverage/src/Report/Html/Renderer/Template/css/nv.d3.min.cssX%:]X%0,Pvendor/phpunit/php-code-coverage/src/Report/Html/Renderer/Template/css/style.css+:]+Y`gVvendor/phpunit/php-code-coverage/src/Report/Html/Renderer/Template/dashboard.html.dist:]6 Vvendor/phpunit/php-code-coverage/src/Report/Html/Renderer/Template/directory.html.dist!:]!!_[vendor/phpunit/php-code-coverage/src/Report/Html/Renderer/Template/directory_item.html.dist5:]5Z]Qvendor/phpunit/php-code-coverage/src/Report/Html/Renderer/Template/file.html.dist :] (ﷶVvendor/phpunit/php-code-coverage/src/Report/Html/Renderer/Template/file_item.html.distg:]gV Pivendor/phpunit/php-code-coverage/src/Report/Html/Renderer/Template/fonts/glyphicons-halflings-regular.eotN:]NXDZivendor/phpunit/php-code-coverage/src/Report/Html/Renderer/Template/fonts/glyphicons-halflings-regular.svg¨:]¨|ɶivendor/phpunit/php-code-coverage/src/Report/Html/Renderer/Template/fonts/glyphicons-halflings-regular.ttf\:]\<jvendor/phpunit/php-code-coverage/src/Report/Html/Renderer/Template/fonts/glyphicons-halflings-regular.woff[:][{kvendor/phpunit/php-code-coverage/src/Report/Html/Renderer/Template/fonts/glyphicons-halflings-regular.woff2lF:]lFvaVvendor/phpunit/php-code-coverage/src/Report/Html/Renderer/Template/js/bootstrap.min.js:]/jOvendor/phpunit/php-code-coverage/src/Report/Html/Renderer/Template/js/d3.min.jsP:]PhbSvendor/phpunit/php-code-coverage/src/Report/Html/Renderer/Template/js/holder.min.jsm:]mJsѶVvendor/phpunit/php-code-coverage/src/Report/Html/Renderer/Template/js/html5shiv.min.js[(:][( ü,Svendor/phpunit/php-code-coverage/src/Report/Html/Renderer/Template/js/jquery.min.jsR:]R~Rvendor/phpunit/php-code-coverage/src/Report/Html/Renderer/Template/js/nv.d3.min.jsR:]Rvendor/phpunit/phpunit/src/Framework/CodeCoverageException.phpe:]eX$-57vendor/phpunit/phpunit/src/Framework/Constraint/And.php:] #?vendor/phpunit/phpunit/src/Framework/Constraint/ArrayHasKey.phpt:]tMM¶?vendor/phpunit/phpunit/src/Framework/Constraint/ArraySubset.php :] 1=vendor/phpunit/phpunit/src/Framework/Constraint/Attribute.php:]+<vendor/phpunit/phpunit/src/Framework/Constraint/Callback.php:]X`lEvendor/phpunit/phpunit/src/Framework/Constraint/ClassHasAttribute.php:]ԸKvendor/phpunit/phpunit/src/Framework/Constraint/ClassHasStaticAttribute.php:]F=vendor/phpunit/phpunit/src/Framework/Constraint/Composite.php:]|+9vendor/phpunit/phpunit/src/Framework/Constraint/Count.php@:]@4ĶCvendor/phpunit/phpunit/src/Framework/Constraint/DirectoryExists.phpf:]f)Y =vendor/phpunit/phpunit/src/Framework/Constraint/Exception.php:] Avendor/phpunit/phpunit/src/Framework/Constraint/ExceptionCode.phpu:]u)1Dvendor/phpunit/phpunit/src/Framework/Constraint/ExceptionMessage.phpm:]mJYJvendor/phpunit/phpunit/src/Framework/Constraint/ExceptionMessageRegExp.php7:]7o>vendor/phpunit/phpunit/src/Framework/Constraint/FileExists.php\:]\z?vendor/phpunit/phpunit/src/Framework/Constraint/GreaterThan.php:] Y">vendor/phpunit/phpunit/src/Framework/Constraint/IsAnything.phpU:]U42;vendor/phpunit/phpunit/src/Framework/Constraint/IsEmpty.php:]GF;vendor/phpunit/phpunit/src/Framework/Constraint/IsEqual.php:]$;vendor/phpunit/phpunit/src/Framework/Constraint/IsFalse.php:]ؒ<vendor/phpunit/phpunit/src/Framework/Constraint/IsFinite.php:]#9?vendor/phpunit/phpunit/src/Framework/Constraint/IsIdentical.php|:]|d>vendor/phpunit/phpunit/src/Framework/Constraint/IsInfinite.php:]6޼߶@vendor/phpunit/phpunit/src/Framework/Constraint/IsInstanceOf.phpE:]E:vendor/phpunit/phpunit/src/Framework/Constraint/IsJson.php:]#9vendor/phpunit/phpunit/src/Framework/Constraint/IsNan.php:]06[:vendor/phpunit/phpunit/src/Framework/Constraint/IsNull.php:]@~>vendor/phpunit/phpunit/src/Framework/Constraint/IsReadable.php\:]\Ic:vendor/phpunit/phpunit/src/Framework/Constraint/IsTrue.php:]9:vendor/phpunit/phpunit/src/Framework/Constraint/IsType.php:]rMe>vendor/phpunit/phpunit/src/Framework/Constraint/IsWritable.php\:]\STvendor/phpunit/phpunit/src/Framework/Constraint/JsonMatches/ErrorMessageProvider.php:](`=?vendor/phpunit/phpunit/src/Framework/Constraint/JsonMatches.php9:]9ا'<vendor/phpunit/phpunit/src/Framework/Constraint/LessThan.php:]ekR7vendor/phpunit/phpunit/src/Framework/Constraint/Not.php:]EX\Fvendor/phpunit/phpunit/src/Framework/Constraint/ObjectHasAttribute.php:] C6vendor/phpunit/phpunit/src/Framework/Constraint/Or.phpd:]d\=vendor/phpunit/phpunit/src/Framework/Constraint/PCREMatch.php:]2<u<vendor/phpunit/phpunit/src/Framework/Constraint/SameSize.php:]8Bvendor/phpunit/phpunit/src/Framework/Constraint/StringContains.php:]dӶBvendor/phpunit/phpunit/src/Framework/Constraint/StringEndsWith.php:]VAvendor/phpunit/phpunit/src/Framework/Constraint/StringMatches.phpO:]OPDvendor/phpunit/phpunit/src/Framework/Constraint/StringStartsWith.php:]Q߶Gvendor/phpunit/phpunit/src/Framework/Constraint/TraversableContains.php:]D=Kvendor/phpunit/phpunit/src/Framework/Constraint/TraversableContainsOnly.phpj:]j@(7vendor/phpunit/phpunit/src/Framework/Constraint/Xor.php:] 33vendor/phpunit/phpunit/src/Framework/Constraint.php:]OǶHvendor/phpunit/phpunit/src/Framework/CoveredCodeNotExecutedException.phpx:]xޙ9vendor/phpunit/phpunit/src/Framework/Error/Deprecated.php:]o5vendor/phpunit/phpunit/src/Framework/Error/Notice.php:]r56vendor/phpunit/phpunit/src/Framework/Error/Warning.php:] Ͷ.vendor/phpunit/phpunit/src/Framework/Error.php:]V%2vendor/phpunit/phpunit/src/Framework/Exception.php.:].Ie"9vendor/phpunit/phpunit/src/Framework/ExceptionWrapper.php:].Cvendor/phpunit/phpunit/src/Framework/ExpectationFailedException.php:]j#7vendor/phpunit/phpunit/src/Framework/IncompleteTest.phpB:]Bs0;vendor/phpunit/phpunit/src/Framework/IncompleteTestCase.php:]pO<vendor/phpunit/phpunit/src/Framework/IncompleteTestError.php:]^~Evendor/phpunit/phpunit/src/Framework/InvalidCoversTargetException.phpx:]xɯƶIvendor/phpunit/phpunit/src/Framework/MissingCoversAnnotationException.phpy:]y=T4vendor/phpunit/phpunit/src/Framework/OutputError.phpj:]j@6;2vendor/phpunit/phpunit/src/Framework/RiskyTest.php=:]=p}7vendor/phpunit/phpunit/src/Framework/RiskyTestError.php:]!7vendor/phpunit/phpunit/src/Framework/SelfDescribing.phpb:]b4vendor/phpunit/phpunit/src/Framework/SkippedTest.php>:]>BIl8vendor/phpunit/phpunit/src/Framework/SkippedTestCase.php:]&Ͷ9vendor/phpunit/phpunit/src/Framework/SkippedTestError.php:]?>vendor/phpunit/phpunit/src/Framework/SkippedTestSuiteError.php:]e7vendor/phpunit/phpunit/src/Framework/SyntheticError.php:]0-vendor/phpunit/phpunit/src/Framework/Test.php:]%r1vendor/phpunit/phpunit/src/Framework/TestCase.php:]D4vendor/phpunit/phpunit/src/Framework/TestFailure.php4:]4p5vendor/phpunit/phpunit/src/Framework/TestListener.php<:]<r3vendor/phpunit/phpunit/src/Framework/TestResult.php H:] HH?vendor/phpunit/phpunit/src/Framework/TestSuite/DataProvider.php:].)˶2vendor/phpunit/phpunit/src/Framework/TestSuite.phpx9:]x9 Hvendor/phpunit/phpunit/src/Framework/UnintentionallyCoveredCodeError.phpx:]xOb0vendor/phpunit/phpunit/src/Framework/Warning.php:]Ik8vendor/phpunit/phpunit/src/Framework/WarningTestCase.phpf:]f4vendor/phpunit/phpunit/src/Runner/BaseTestRunner.php&:]&W,/vendor/phpunit/phpunit/src/Runner/Exception.phph:]h4vendor/phpunit/phpunit/src/Runner/Filter/Factory.phpX:]X:vendor/phpunit/phpunit/src/Runner/Filter/Group/Exclude.php:]:vendor/phpunit/phpunit/src/Runner/Filter/Group/Include.php:]i42vendor/phpunit/phpunit/src/Runner/Filter/Group.php :] 6f1vendor/phpunit/phpunit/src/Runner/Filter/Test.php&:]&Ek=vendor/phpunit/phpunit/src/Runner/StandardTestSuiteLoader.php:]Y5vendor/phpunit/phpunit/src/Runner/TestSuiteLoader.php:]Vτ-vendor/phpunit/phpunit/src/Runner/Version.phpe:]edN -vendor/phpunit/phpunit/src/TextUI/Command.phprd:]rds03vendor/phpunit/phpunit/src/TextUI/ResultPrinter.php(:](0vendor/phpunit/phpunit/src/TextUI/TestRunner.php}:]} }-vendor/phpunit/phpunit/src/Util/Blacklist.php:]>91vendor/phpunit/phpunit/src/Util/Configuration.php#K:]#KT:vendor/phpunit/phpunit/src/Util/ConfigurationGenerator.php:]20vendor/phpunit/phpunit/src/Util/ErrorHandler.php:]} .vendor/phpunit/phpunit/src/Util/Fileloader.php4:]4 vendor/phpunit/phpunit/src/Util/TestDox/ResultPrinter/HTML.php:]=f>vendor/phpunit/phpunit/src/Util/TestDox/ResultPrinter/Text.php:]<$=vendor/phpunit/phpunit/src/Util/TestDox/ResultPrinter/XML.php :] ̶9vendor/phpunit/phpunit/src/Util/TestDox/ResultPrinter.php:]ai5vendor/phpunit/phpunit/src/Util/TestSuiteIterator.php:]/(vendor/phpunit/phpunit/src/Util/Type.php:]g誶'vendor/phpunit/phpunit/src/Util/XML.php :]  "Qvendor/phpunit/phpunit-mock-objects/src/Framework/MockObject/Builder/Identity.phpu:]udSֶYvendor/phpunit/phpunit-mock-objects/src/Framework/MockObject/Builder/InvocationMocker.php:]Nvendor/phpunit/phpunit-mock-objects/src/Framework/MockObject/Builder/Match.php:]SXvendor/phpunit/phpunit-mock-objects/src/Framework/MockObject/Builder/MethodNameMatch.php:]Rvendor/phpunit/phpunit-mock-objects/src/Framework/MockObject/Builder/Namespace.php:]gXvendor/phpunit/phpunit-mock-objects/src/Framework/MockObject/Builder/ParametersMatch.php:]Y2Mvendor/phpunit/phpunit-mock-objects/src/Framework/MockObject/Builder/Stub.php:]QPQdavendor/phpunit/phpunit-mock-objects/src/Framework/MockObject/Exception/BadMethodCallException.php:]˥Tvendor/phpunit/phpunit-mock-objects/src/Framework/MockObject/Exception/Exception.phpI:]I;[vendor/phpunit/phpunit-mock-objects/src/Framework/MockObject/Exception/RuntimeException.php:]˹} [vendor/phpunit/phpunit-mock-objects/src/Framework/MockObject/Generator/deprecation.tpl.dist;:];O5s\vendor/phpunit/phpunit-mock-objects/src/Framework/MockObject/Generator/mocked_class.tpl.dist:]zQcvendor/phpunit/phpunit-mock-objects/src/Framework/MockObject/Generator/mocked_class_method.tpl.dist:]4޶\vendor/phpunit/phpunit-mock-objects/src/Framework/MockObject/Generator/mocked_clone.tpl.dist:]aT]vendor/phpunit/phpunit-mock-objects/src/Framework/MockObject/Generator/mocked_method.tpl.dist:]bvendor/phpunit/phpunit-mock-objects/src/Framework/MockObject/Generator/mocked_method_void.tpl.dist:]~G'ڶdvendor/phpunit/phpunit-mock-objects/src/Framework/MockObject/Generator/mocked_static_method.tpl.dist:]N^vendor/phpunit/phpunit-mock-objects/src/Framework/MockObject/Generator/proxied_method.tpl.dist:] cvendor/phpunit/phpunit-mock-objects/src/Framework/MockObject/Generator/proxied_method_void.tpl.dist:]gEm[vendor/phpunit/phpunit-mock-objects/src/Framework/MockObject/Generator/trait_class.tpl.dist7:]7[$~^vendor/phpunit/phpunit-mock-objects/src/Framework/MockObject/Generator/unmocked_clone.tpl.dist:]8W}ضZvendor/phpunit/phpunit-mock-objects/src/Framework/MockObject/Generator/wsdl_class.tpl.dist:]w&S[vendor/phpunit/phpunit-mock-objects/src/Framework/MockObject/Generator/wsdl_method.tpl.dist<:]<iJvendor/phpunit/phpunit-mock-objects/src/Framework/MockObject/Generator.phpJa:]JaBRvendor/phpunit/phpunit-mock-objects/src/Framework/MockObject/Invocation/Object.php:]BdRvendor/phpunit/phpunit-mock-objects/src/Framework/MockObject/Invocation/Static.php :] h=XKvendor/phpunit/phpunit-mock-objects/src/Framework/MockObject/Invocation.phpt:]t" Qvendor/phpunit/phpunit-mock-objects/src/Framework/MockObject/InvocationMocker.php:]ҁlJvendor/phpunit/phpunit-mock-objects/src/Framework/MockObject/Invokable.php':]'ɶXvendor/phpunit/phpunit-mock-objects/src/Framework/MockObject/Matcher/AnyInvokedCount.php:]pVvendor/phpunit/phpunit-mock-objects/src/Framework/MockObject/Matcher/AnyParameters.php6:]6xp^vendor/phpunit/phpunit-mock-objects/src/Framework/MockObject/Matcher/ConsecutiveParameters.php :] ;MXSvendor/phpunit/phpunit-mock-objects/src/Framework/MockObject/Matcher/Invocation.phpZ:]ZxZ3Wvendor/phpunit/phpunit-mock-objects/src/Framework/MockObject/Matcher/InvokedAtIndex.phpr:]r\vendor/phpunit/phpunit-mock-objects/src/Framework/MockObject/Matcher/InvokedAtLeastCount.php:]~i[vendor/phpunit/phpunit-mock-objects/src/Framework/MockObject/Matcher/InvokedAtLeastOnce.php:]0S[vendor/phpunit/phpunit-mock-objects/src/Framework/MockObject/Matcher/InvokedAtMostCount.php:]#_?6Uvendor/phpunit/phpunit-mock-objects/src/Framework/MockObject/Matcher/InvokedCount.php":]"9Xvendor/phpunit/phpunit-mock-objects/src/Framework/MockObject/Matcher/InvokedRecorder.phpz:]z1ISvendor/phpunit/phpunit-mock-objects/src/Framework/MockObject/Matcher/MethodName.php:]3vSvendor/phpunit/phpunit-mock-objects/src/Framework/MockObject/Matcher/Parameters.phph :]h ߀d9\vendor/phpunit/phpunit-mock-objects/src/Framework/MockObject/Matcher/StatelessInvocation.php':]'ZyöHvendor/phpunit/phpunit-mock-objects/src/Framework/MockObject/Matcher.php:]_Lvendor/phpunit/phpunit-mock-objects/src/Framework/MockObject/MockBuilder.php,:],8IKvendor/phpunit/phpunit-mock-objects/src/Framework/MockObject/MockObject.phpz:]zLs`tVvendor/phpunit/phpunit-mock-objects/src/Framework/MockObject/Stub/ConsecutiveCalls.php:]$鬶Ovendor/phpunit/phpunit-mock-objects/src/Framework/MockObject/Stub/Exception.php:]k1Wvendor/phpunit/phpunit-mock-objects/src/Framework/MockObject/Stub/MatcherCollection.php:]*Lvendor/phpunit/phpunit-mock-objects/src/Framework/MockObject/Stub/Return.php:]jPy@Tvendor/phpunit/phpunit-mock-objects/src/Framework/MockObject/Stub/ReturnArgument.php%:]%0]Tvendor/phpunit/phpunit-mock-objects/src/Framework/MockObject/Stub/ReturnCallback.php8:]8?'FUvendor/phpunit/phpunit-mock-objects/src/Framework/MockObject/Stub/ReturnReference.php:]%Pvendor/phpunit/phpunit-mock-objects/src/Framework/MockObject/Stub/ReturnSelf.php:]+߶Tvendor/phpunit/phpunit-mock-objects/src/Framework/MockObject/Stub/ReturnValueMap.phpt:]teJLEvendor/phpunit/phpunit-mock-objects/src/Framework/MockObject/Stub.php:]ECKvendor/phpunit/phpunit-mock-objects/src/Framework/MockObject/Verifiable.phpj:]j !vendor/predis/predis/autoload.phpW:]Wf4C<vendor/predis/predis/examples/custom_cluster_distributor.php:]m)Ӷ7vendor/predis/predis/examples/debuggable_connection.php:]ɶ1vendor/predis/predis/examples/dispatcher_loop.php:]!1:vendor/predis/predis/examples/executing_redis_commands.php(:](NY/vendor/predis/predis/examples/key_prefixing.php :] F0;vendor/predis/predis/examples/lua_scripting_abstraction.phpW:]WmcPg2vendor/predis/predis/examples/monitor_consumer.php:]I05vendor/predis/predis/examples/pipelining_commands.phpj:]jҶ1vendor/predis/predis/examples/pubsub_consumer.phpa:]aϕ=vendor/predis/predis/examples/redis_collections_iterators.phpR:]R3oy"5vendor/predis/predis/examples/replication_complex.php:]SQ<6vendor/predis/predis/examples/replication_sentinel.phpL:]L 4vendor/predis/predis/examples/replication_simple.php-:]-ǿ1vendor/predis/predis/examples/session_handler.php:] I(vendor/predis/predis/examples/shared.phpA:]AyE۶7vendor/predis/predis/examples/transaction_using_cas.php :] /T0'vendor/predis/predis/src/Autoloader.php:]윝#vendor/predis/predis/src/Client.php:]-U3vendor/predis/predis/src/ClientContextInterface.php:]zӶ,vendor/predis/predis/src/ClientException.phpZ:]Zl<,vendor/predis/predis/src/ClientInterface.php:]J4vendor/predis/predis/src/Cluster/ClusterStrategy.php#:]# @Evendor/predis/predis/src/Cluster/Distributor/DistributorInterface.php:]jCvendor/predis/predis/src/Cluster/Distributor/EmptyRingException.phpl:]l9vendor/predis/predis/src/Cluster/Distributor/HashRing.php :] s,;vendor/predis/predis/src/Cluster/Distributor/KetamaRing.phpv:]vx/vendor/predis/predis/src/Cluster/Hash/CRC16.phpr :]r dѶ@vendor/predis/predis/src/Cluster/Hash/HashGeneratorInterface.php:]!)3vendor/predis/predis/src/Cluster/PredisStrategy.php:]s2vendor/predis/predis/src/Cluster/RedisStrategy.php:] m6vendor/predis/predis/src/Cluster/StrategyInterface.php?:]?"Dvendor/predis/predis/src/Collection/Iterator/CursorBasedIterator.php:]N@8vendor/predis/predis/src/Collection/Iterator/HashKey.php:]4 9vendor/predis/predis/src/Collection/Iterator/Keyspace.php:]Rs8vendor/predis/predis/src/Collection/Iterator/ListKey.php:]#ذ7vendor/predis/predis/src/Collection/Iterator/SetKey.php:]t=vendor/predis/predis/src/Collection/Iterator/SortedSetKey.php:]v>,vendor/predis/predis/src/Command/Command.php:]5vendor/predis/predis/src/Command/CommandInterface.php:]'i3vendor/predis/predis/src/Command/ConnectionAuth.php:]On3vendor/predis/predis/src/Command/ConnectionEcho.php:]Bg3vendor/predis/predis/src/Command/ConnectionPing.php:]<3vendor/predis/predis/src/Command/ConnectionQuit.php:]I5vendor/predis/predis/src/Command/ConnectionSelect.php:]=3d5vendor/predis/predis/src/Command/GeospatialGeoAdd.phpr:]r퓭6vendor/predis/predis/src/Command/GeospatialGeoDist.php:]Nv@6vendor/predis/predis/src/Command/GeospatialGeoHash.phpl:]lqa5vendor/predis/predis/src/Command/GeospatialGeoPos.phpj:]jYö8vendor/predis/predis/src/Command/GeospatialGeoRadius.php:]͐@vendor/predis/predis/src/Command/GeospatialGeoRadiusByMember.php:] /vendor/predis/predis/src/Command/HashDelete.php:] /vendor/predis/predis/src/Command/HashExists.php:],vendor/predis/predis/src/Command/HashGet.php:]@ض/vendor/predis/predis/src/Command/HashGetAll.php$:]$Q4vendor/predis/predis/src/Command/HashGetMultiple.php:]П4vendor/predis/predis/src/Command/HashIncrementBy.php:]GHo9vendor/predis/predis/src/Command/HashIncrementByFloat.php:]N`+-vendor/predis/predis/src/Command/HashKeys.php:]d</vendor/predis/predis/src/Command/HashLength.php:]y-vendor/predis/predis/src/Command/HashScan.php:]74,vendor/predis/predis/src/Command/HashSet.php:]HK޶4vendor/predis/predis/src/Command/HashSetMultiple.php:]U4vendor/predis/predis/src/Command/HashSetPreserve.php:]q::5vendor/predis/predis/src/Command/HashStringLength.php:]:|r/vendor/predis/predis/src/Command/HashValues.php:]U F[3vendor/predis/predis/src/Command/HyperLogLogAdd.php:]5vendor/predis/predis/src/Command/HyperLogLogCount.php:].=^5vendor/predis/predis/src/Command/HyperLogLogMerge.php:]hs.vendor/predis/predis/src/Command/KeyDelete.php:]}aж,vendor/predis/predis/src/Command/KeyDump.php:]j߶.vendor/predis/predis/src/Command/KeyExists.php:]h.vendor/predis/predis/src/Command/KeyExpire.php:]4 0vendor/predis/predis/src/Command/KeyExpireAt.php:]^,vendor/predis/predis/src/Command/KeyKeys.php:]/vendor/predis/predis/src/Command/KeyMigrate.php:][V,vendor/predis/predis/src/Command/KeyMove.php:]X/vendor/predis/predis/src/Command/KeyPersist.php:]ɽ5vendor/predis/predis/src/Command/KeyPreciseExpire.php:]|7vendor/predis/predis/src/Command/KeyPreciseExpireAt.php:]!9vendor/predis/predis/src/Command/KeyPreciseTimeToLive.php:]0&.vendor/predis/predis/src/Command/KeyRandom.php:]}3).vendor/predis/predis/src/Command/KeyRename.php:]E߇ζ6vendor/predis/predis/src/Command/KeyRenamePreserve.php:].LT/vendor/predis/predis/src/Command/KeyRestore.php:]fI?޶,vendor/predis/predis/src/Command/KeyScan.php:]p,vendor/predis/predis/src/Command/KeySort.phpY:]Y=2vendor/predis/predis/src/Command/KeyTimeToLive.php:]<:,vendor/predis/predis/src/Command/KeyType.php:].vendor/predis/predis/src/Command/ListIndex.php:]۬ö/vendor/predis/predis/src/Command/ListInsert.php:]WtK/vendor/predis/predis/src/Command/ListLength.php:]c91vendor/predis/predis/src/Command/ListPopFirst.php:]L9vendor/predis/predis/src/Command/ListPopFirstBlocking.phpf:]fɝն0vendor/predis/predis/src/Command/ListPopLast.php:]8vendor/predis/predis/src/Command/ListPopLastBlocking.php:]{,8vendor/predis/predis/src/Command/ListPopLastPushHead.php:]Ų@vendor/predis/predis/src/Command/ListPopLastPushHeadBlocking.php:]=1vendor/predis/predis/src/Command/ListPushHead.php:]92vendor/predis/predis/src/Command/ListPushHeadX.php:]_f|1vendor/predis/predis/src/Command/ListPushTail.php:]2vendor/predis/predis/src/Command/ListPushTailX.php:]gjܟ.vendor/predis/predis/src/Command/ListRange.php:]϶/vendor/predis/predis/src/Command/ListRemove.php:]',vendor/predis/predis/src/Command/ListSet.php:]}-vendor/predis/predis/src/Command/ListTrim.php:] Q?vendor/predis/predis/src/Command/PrefixableCommandInterface.php:]ūAvendor/predis/predis/src/Command/Processor/KeyPrefixProcessor.php:]Fdo=vendor/predis/predis/src/Command/Processor/ProcessorChain.php:]cfAvendor/predis/predis/src/Command/Processor/ProcessorInterface.php:]Ɛc2vendor/predis/predis/src/Command/PubSubPublish.php:]1vendor/predis/predis/src/Command/PubSubPubsub.php :] =vζ4vendor/predis/predis/src/Command/PubSubSubscribe.php:]Qv=vendor/predis/predis/src/Command/PubSubSubscribeByPattern.php:]t|6vendor/predis/predis/src/Command/PubSubUnsubscribe.php:]|?vendor/predis/predis/src/Command/PubSubUnsubscribeByPattern.php:]a @/vendor/predis/predis/src/Command/RawCommand.php:]6eA2vendor/predis/predis/src/Command/ScriptCommand.php:]϶?vendor/predis/predis/src/Command/ServerBackgroundRewriteAOF.php :] j_9vendor/predis/predis/src/Command/ServerBackgroundSave.php:]B1vendor/predis/predis/src/Command/ServerClient.php:]2vendor/predis/predis/src/Command/ServerCommand.php:]kӶ1vendor/predis/predis/src/Command/ServerConfig.phpP:]Pr7vendor/predis/predis/src/Command/ServerDatabaseSize.php:]8/vendor/predis/predis/src/Command/ServerEval.php:]e92vendor/predis/predis/src/Command/ServerEvalSHA.php:]Jn3vendor/predis/predis/src/Command/ServerFlushAll.php:]ː8vendor/predis/predis/src/Command/ServerFlushDatabase.php:]G/vendor/predis/predis/src/Command/ServerInfo.php:]4}޶3vendor/predis/predis/src/Command/ServerInfoV26x.phpc:]cl3vendor/predis/predis/src/Command/ServerLastSave.php:]eWW2vendor/predis/predis/src/Command/ServerMonitor.php:]&#$1vendor/predis/predis/src/Command/ServerObject.php:]9V/vendor/predis/predis/src/Command/ServerSave.php:]{̶1vendor/predis/predis/src/Command/ServerScript.php:]AV^3vendor/predis/predis/src/Command/ServerSentinel.phpf:]f)b3vendor/predis/predis/src/Command/ServerShutdown.php:]2vendor/predis/predis/src/Command/ServerSlaveOf.php4:]4JgӶ2vendor/predis/predis/src/Command/ServerSlowlog.php:]t/vendor/predis/predis/src/Command/ServerTime.php:]쒶+vendor/predis/predis/src/Command/SetAdd.php:]Ä3vendor/predis/predis/src/Command/SetCardinality.php:]ͯ!2vendor/predis/predis/src/Command/SetDifference.php:]p7vendor/predis/predis/src/Command/SetDifferenceStore.php:] 4vendor/predis/predis/src/Command/SetIntersection.php:]9vendor/predis/predis/src/Command/SetIntersectionStore.phpZ:]ZBW0vendor/predis/predis/src/Command/SetIsMember.php:]5,/vendor/predis/predis/src/Command/SetMembers.php:]@,vendor/predis/predis/src/Command/SetMove.php:]RAS+vendor/predis/predis/src/Command/SetPop.php:])o¶4vendor/predis/predis/src/Command/SetRandomMember.php:]`/.vendor/predis/predis/src/Command/SetRemove.php:]Wq,vendor/predis/predis/src/Command/SetScan.php:]E-vendor/predis/predis/src/Command/SetUnion.php:]X2vendor/predis/predis/src/Command/SetUnionStore.php:]S-1vendor/predis/predis/src/Command/StringAppend.php:]پ3vendor/predis/predis/src/Command/StringBitCount.php:]l 3vendor/predis/predis/src/Command/StringBitField.php:]ٙӶ0vendor/predis/predis/src/Command/StringBitOp.php:] 1vendor/predis/predis/src/Command/StringBitPos.php:]4vendor/predis/predis/src/Command/StringDecrement.php:]KԶ6vendor/predis/predis/src/Command/StringDecrementBy.php:];.vendor/predis/predis/src/Command/StringGet.php:]1vendor/predis/predis/src/Command/StringGetBit.php:]B6vendor/predis/predis/src/Command/StringGetMultiple.php:])Ͷ3vendor/predis/predis/src/Command/StringGetRange.php:]1vendor/predis/predis/src/Command/StringGetSet.php:]m4vendor/predis/predis/src/Command/StringIncrement.php:]);wĶ6vendor/predis/predis/src/Command/StringIncrementBy.php:]uS;vendor/predis/predis/src/Command/StringIncrementByFloat.php:]q놶;vendor/predis/predis/src/Command/StringPreciseSetExpire.php:]hv.vendor/predis/predis/src/Command/StringSet.php:]p1vendor/predis/predis/src/Command/StringSetBit.php:]:їB4vendor/predis/predis/src/Command/StringSetExpire.php:]L,6vendor/predis/predis/src/Command/StringSetMultiple.php:]d>vendor/predis/predis/src/Command/StringSetMultiplePreserve.php:] ɶ6vendor/predis/predis/src/Command/StringSetPreserve.php:]F3vendor/predis/predis/src/Command/StringSetRange.php:]e^1vendor/predis/predis/src/Command/StringStrlen.php:]?1vendor/predis/predis/src/Command/StringSubstr.php:]!7vendor/predis/predis/src/Command/TransactionDiscard.php:]t4vendor/predis/predis/src/Command/TransactionExec.php:]Z-255vendor/predis/predis/src/Command/TransactionMulti.php:] U7vendor/predis/predis/src/Command/TransactionUnwatch.php:]5vendor/predis/predis/src/Command/TransactionWatch.php*:]*,vendor/predis/predis/src/Command/ZSetAdd.php\:]\R4vendor/predis/predis/src/Command/ZSetCardinality.php:]URٶ.vendor/predis/predis/src/Command/ZSetCount.php:]/+4vendor/predis/predis/src/Command/ZSetIncrementBy.php:]K/:vendor/predis/predis/src/Command/ZSetIntersectionStore.php:],_1vendor/predis/predis/src/Command/ZSetLexCount.php:]J-ٶ.vendor/predis/predis/src/Command/ZSetRange.php:]>33vendor/predis/predis/src/Command/ZSetRangeByLex.php:]". 5vendor/predis/predis/src/Command/ZSetRangeByScore.phpy:]y`®-vendor/predis/predis/src/Command/ZSetRank.php:]h/vendor/predis/predis/src/Command/ZSetRemove.php:]z}9vendor/predis/predis/src/Command/ZSetRemoveRangeByLex.php:]fb:vendor/predis/predis/src/Command/ZSetRemoveRangeByRank.php:]! T;vendor/predis/predis/src/Command/ZSetRemoveRangeByScore.php:]F5vendor/predis/predis/src/Command/ZSetReverseRange.php:]zh5:vendor/predis/predis/src/Command/ZSetReverseRangeByLex.php:]cQ<vendor/predis/predis/src/Command/ZSetReverseRangeByScore.php:]-Ŷ4vendor/predis/predis/src/Command/ZSetReverseRank.php:],-vendor/predis/predis/src/Command/ZSetScan.php:]7j.vendor/predis/predis/src/Command/ZSetScore.php:]Ŷ3vendor/predis/predis/src/Command/ZSetUnionStore.php:]c/3vendor/predis/predis/src/CommunicationException.php:]8vendor/predis/predis/src/Configuration/ClusterOption.php:]f:Bvendor/predis/predis/src/Configuration/ConnectionFactoryOption.php:]m"ƶ;vendor/predis/predis/src/Configuration/ExceptionsOption.php-:]-g:vendor/predis/predis/src/Configuration/OptionInterface.php:]v 2vendor/predis/predis/src/Configuration/Options.phpl:]l BM;vendor/predis/predis/src/Configuration/OptionsInterface.php:]p7vendor/predis/predis/src/Configuration/PrefixOption.php:]F=8vendor/predis/predis/src/Configuration/ProfileOption.php}:]}"<vendor/predis/predis/src/Configuration/ReplicationOption.php:]ʛ:vendor/predis/predis/src/Connection/AbstractConnection.phpQ :]Q Bvendor/predis/predis/src/Connection/Aggregate/ClusterInterface.php:]NHvendor/predis/predis/src/Connection/Aggregate/MasterSlaveReplication.phpW:]W.%?vendor/predis/predis/src/Connection/Aggregate/PredisCluster.php :] T7>vendor/predis/predis/src/Connection/Aggregate/RedisCluster.php#:]#qFvendor/predis/predis/src/Connection/Aggregate/ReplicationInterface.php:]!Evendor/predis/predis/src/Connection/Aggregate/SentinelReplication.php2':]2'Dvendor/predis/predis/src/Connection/AggregateConnectionInterface.php:]lAuDvendor/predis/predis/src/Connection/CompositeConnectionInterface.php:]]Avendor/predis/predis/src/Connection/CompositeStreamConnection.php:]a;vendor/predis/predis/src/Connection/ConnectionException.php:]-;vendor/predis/predis/src/Connection/ConnectionInterface.php:]^ M/vendor/predis/predis/src/Connection/Factory.phpD :]D v8vendor/predis/predis/src/Connection/FactoryInterface.php;:];6.5?vendor/predis/predis/src/Connection/NodeConnectionInterface.phpm:]mi2vendor/predis/predis/src/Connection/Parameters.php:]6a;vendor/predis/predis/src/Connection/ParametersInterface.php:] YֶAvendor/predis/predis/src/Connection/PhpiredisSocketConnection.php\:]\:ݶAvendor/predis/predis/src/Connection/PhpiredisStreamConnection.php.:].< $8vendor/predis/predis/src/Connection/StreamConnection.php:]T /8vendor/predis/predis/src/Connection/WebdisConnection.phpZ:]ZR-vendor/predis/predis/src/Monitor/Consumer.php:]3c2vendor/predis/predis/src/NotSupportedException.phpa:]an),vendor/predis/predis/src/Pipeline/Atomic.php> :]> vjw:vendor/predis/predis/src/Pipeline/ConnectionErrorProof.php :] 73vendor/predis/predis/src/Pipeline/FireAndForget.phpf:]fj6.vendor/predis/predis/src/Pipeline/Pipeline.php :] |#v,vendor/predis/predis/src/PredisException.php^:]^,vendor/predis/predis/src/Profile/Factory.php:]&r5vendor/predis/predis/src/Profile/ProfileInterface.php\:]\2z1vendor/predis/predis/src/Profile/RedisProfile.php:];2vendor/predis/predis/src/Profile/RedisUnstable.php:]͋4vendor/predis/predis/src/Profile/RedisVersion200.php*:]*$z4vendor/predis/predis/src/Profile/RedisVersion220.php:]}$U4vendor/predis/predis/src/Profile/RedisVersion240.php(:](M`D4vendor/predis/predis/src/Profile/RedisVersion260.php:])\R4vendor/predis/predis/src/Profile/RedisVersion280.php:]_۶4vendor/predis/predis/src/Profile/RedisVersion300.php:]֦4vendor/predis/predis/src/Profile/RedisVersion320.phpF:]FG.C7vendor/predis/predis/src/Protocol/ProtocolException.php:]5@vendor/predis/predis/src/Protocol/ProtocolProcessorInterface.php_:]_i"@vendor/predis/predis/src/Protocol/RequestSerializerInterface.php:]n=vendor/predis/predis/src/Protocol/ResponseReaderInterface.php:]7Evendor/predis/predis/src/Protocol/Text/CompositeProtocolProcessor.php:],$?vendor/predis/predis/src/Protocol/Text/Handler/BulkResponse.php:]W0@vendor/predis/predis/src/Protocol/Text/Handler/ErrorResponse.php;:];LIBvendor/predis/predis/src/Protocol/Text/Handler/IntegerResponse.php`:]`LDvendor/predis/predis/src/Protocol/Text/Handler/MultiBulkResponse.php :] :dKvendor/predis/predis/src/Protocol/Text/Handler/ResponseHandlerInterface.php:] wQAvendor/predis/predis/src/Protocol/Text/Handler/StatusResponse.php@:]@%s|Nvendor/predis/predis/src/Protocol/Text/Handler/StreamableMultiBulkResponse.php:]A-(<vendor/predis/predis/src/Protocol/Text/ProtocolProcessor.php :] {<vendor/predis/predis/src/Protocol/Text/RequestSerializer.phpQ:]Qo9vendor/predis/predis/src/Protocol/Text/ResponseReader.php:]0p4vendor/predis/predis/src/PubSub/AbstractConsumer.php:];_,vendor/predis/predis/src/PubSub/Consumer.php :] XW?"2vendor/predis/predis/src/PubSub/DispatcherLoop.php;:];%?vendor/predis/predis/src/Replication/MissingMasterException.php:]<vendor/predis/predis/src/Replication/ReplicationStrategy.php:]j6vendor/predis/predis/src/Replication/RoleException.php:]״K+vendor/predis/predis/src/Response/Error.php:]&˶4vendor/predis/predis/src/Response/ErrorInterface.php:]`w8vendor/predis/predis/src/Response/Iterator/MultiBulk.php:]#@vendor/predis/predis/src/Response/Iterator/MultiBulkIterator.php:]&=vendor/predis/predis/src/Response/Iterator/MultiBulkTuple.php<:]<r7vendor/predis/predis/src/Response/ResponseInterface.phpQ:]QyZ5vendor/predis/predis/src/Response/ServerException.phpb:]b#,vendor/predis/predis/src/Response/Status.phpR:]Rp|,vendor/predis/predis/src/Session/Handler.phpC:]C!Bvendor/predis/predis/src/Transaction/AbortedMultiExecException.php:]_X2vendor/predis/predis/src/Transaction/MultiExec.php]:]]7vendor/predis/predis/src/Transaction/MultiExecState.php:]C0vendor/psr/http-message/src/MessageInterface.php:])t 0vendor/psr/http-message/src/RequestInterface.php:]V1vendor/psr/http-message/src/ResponseInterface.php:]6vendor/psr/http-message/src/ServerRequestInterface.phpo:]o $h/vendor/psr/http-message/src/StreamInterface.php:]h\l5vendor/psr/http-message/src/UploadedFileInterface.phpz:]z9b,vendor/psr/http-message/src/UriInterface.php|:]|k)vendor/psr/log/Psr/Log/AbstractLogger.php;:];>3[3vendor/psr/log/Psr/Log/InvalidArgumentException.php`:]` X1/vendor/psr/log/Psr/Log/LoggerAwareInterface.php|:]|$+vendor/psr/log/Psr/Log/LoggerAwareTrait.php:]TB*vendor/psr/log/Psr/Log/LoggerInterface.php:]sg&vendor/psr/log/Psr/Log/LoggerTrait.phpi:]i35޶#vendor/psr/log/Psr/Log/LogLevel.php:]j8%vendor/psr/log/Psr/Log/NullLogger.php:]3vendor/psr/log/Psr/Log/Test/LoggerInterfaceTest.phpI :]I As*vendor/psr/log/Psr/Log/Test/TestLogger.php :] c4vendor/ralouphie/getallheaders/src/getallheaders.phpO:]O}r8vendor/sebastian/code-unit-reverse-lookup/src/Wizard.phpZ:]Z3vendor/sebastian/comparator/src/ArrayComparator.php :] kT.vendor/sebastian/comparator/src/Comparator.php:]]9:5vendor/sebastian/comparator/src/ComparisonFailure.php:]e6vendor/sebastian/comparator/src/DateTimeComparator.php:]J5vendor/sebastian/comparator/src/DOMNodeComparator.phpd:]d q^4vendor/sebastian/comparator/src/DoubleComparator.php':]'07vendor/sebastian/comparator/src/ExceptionComparator.php:]`.cG+vendor/sebastian/comparator/src/Factory.php':]'KW8vendor/sebastian/comparator/src/MockObjectComparator.php:]m45vendor/sebastian/comparator/src/NumericComparator.phpL:]LCO4vendor/sebastian/comparator/src/ObjectComparator.phpx:]x%9<6vendor/sebastian/comparator/src/ResourceComparator.php:]]4vendor/sebastian/comparator/src/ScalarComparator.phpb:]bں>vendor/sebastian/comparator/src/SplObjectStorageComparator.phpm:]mX#2vendor/sebastian/comparator/src/TypeComparator.php :] S#vendor/sebastian/diff/src/Chunk.php":]"0"vendor/sebastian/diff/src/Diff.php:]}G$vendor/sebastian/diff/src/Differ.phpq:]qj:vendor/sebastian/diff/src/LCS/LongestCommonSubsequence.php:](UWvendor/sebastian/diff/src/LCS/MemoryEfficientLongestCommonSubsequenceImplementation.php :] cCUvendor/sebastian/diff/src/LCS/TimeEfficientLongestCommonSubsequenceImplementation.php:]m"vendor/sebastian/diff/src/Line.php:]M9ֶ$vendor/sebastian/diff/src/Parser.php:]6a.,vendor/sebastian/environment/src/Console.php*:]*_e,vendor/sebastian/environment/src/Runtime.php1 :]1 o;T*vendor/sebastian/exporter/src/Exporter.php:]D /vendor/sebastian/global-state/src/Blacklist.php:]n|2vendor/sebastian/global-state/src/CodeExporter.php:]4R=/vendor/sebastian/global-state/src/Exception.phpS:]SC).vendor/sebastian/global-state/src/Restorer.php? :]? n̦6vendor/sebastian/global-state/src/RuntimeException.php:]5~.vendor/sebastian/global-state/src/Snapshot.phpU:]U X5vendor/sebastian/object-enumerator/src/Enumerator.php:]K4vendor/sebastian/object-enumerator/src/Exception.phpV:]VcCvendor/sebastian/object-enumerator/src/InvalidArgumentException.php:]2vendor/sebastian/recursion-context/src/Context.phpR:]R^k_4vendor/sebastian/recursion-context/src/Exception.phpX:]XN3Cvendor/sebastian/recursion-context/src/InvalidArgumentException.php:]>_F7vendor/sebastian/resource-operations/build/generate.php:]{K(?vendor/sebastian/resource-operations/src/ResourceOperations.php6:]6 (vendor/sebastian/version/src/Version.phpu:]u*>%vendor/symfony/browser-kit/Client.php%:]%Sж%vendor/symfony/browser-kit/Cookie.php:]ض(vendor/symfony/browser-kit/CookieJar.php :] ١&vendor/symfony/browser-kit/History.php:]}0&vendor/symfony/browser-kit/Request.php:](D'vendor/symfony/browser-kit/Response.php$:]$|&vendor/symfony/console/Application.phpf:]f~Ҷ*vendor/symfony/console/Command/Command.phpw :]w Bv.vendor/symfony/console/Command/HelpCommand.php:]<.vendor/symfony/console/Command/ListCommand.php:]5-0vendor/symfony/console/Command/LockableTrait.php2:]2N߶?vendor/symfony/console/CommandLoader/CommandLoaderInterface.php>:]>eUE?vendor/symfony/console/CommandLoader/ContainerCommandLoader.php?:]? @=vendor/symfony/console/CommandLoader/FactoryCommandLoader.php:]m(vendor/symfony/console/ConsoleEvents.php :]  NնDvendor/symfony/console/DependencyInjection/AddConsoleCommandPass.phpb :]b 蒜<vendor/symfony/console/Descriptor/ApplicationDescription.php2 :]2 M0vendor/symfony/console/Descriptor/Descriptor.phps:]scBM9vendor/symfony/console/Descriptor/DescriptorInterface.php:]rQ4vendor/symfony/console/Descriptor/JsonDescriptor.php:] 8vendor/symfony/console/Descriptor/MarkdownDescriptor.php:]>44vendor/symfony/console/Descriptor/TextDescriptor.php!:]!uD3vendor/symfony/console/Descriptor/XmlDescriptor.php:]&4vendor/symfony/console/Event/ConsoleCommandEvent.php:]!ȶ2vendor/symfony/console/Event/ConsoleErrorEvent.phpb:]bMh-vendor/symfony/console/Event/ConsoleEvent.php:]Ӷ6vendor/symfony/console/Event/ConsoleExceptionEvent.php:]*\ȶ6vendor/symfony/console/Event/ConsoleTerminateEvent.phpz:]z,L6vendor/symfony/console/EventListener/ErrorListener.php:]H=vendor/symfony/console/Exception/CommandNotFoundException.php:]K*57vendor/symfony/console/Exception/ExceptionInterface.phpf:]fAB=vendor/symfony/console/Exception/InvalidArgumentException.php:]̽Z;vendor/symfony/console/Exception/InvalidOptionException.php:]H3vendor/symfony/console/Exception/LogicException.php:]O\e5vendor/symfony/console/Exception/RuntimeException.php:],64vendor/symfony/console/Formatter/OutputFormatter.php:]|?x=vendor/symfony/console/Formatter/OutputFormatterInterface.php:]9vendor/symfony/console/Formatter/OutputFormatterStyle.php:](Bvendor/symfony/console/Formatter/OutputFormatterStyleInterface.php:]G>vendor/symfony/console/Formatter/OutputFormatterStyleStack.php;:];%6vendor/symfony/console/Helper/DebugFormatterHelper.phpj:]jOG 2vendor/symfony/console/Helper/DescriptorHelper.phph:]hK\1vendor/symfony/console/Helper/FormatterHelper.php:])̾(vendor/symfony/console/Helper/Helper.phpk:]k'颶1vendor/symfony/console/Helper/HelperInterface.php:]+vendor/symfony/console/Helper/HelperSet.phpf:]fx2vendor/symfony/console/Helper/InputAwareHelper.phpc:]c/vendor/symfony/console/Helper/ProcessHelper.phpQ :]Q ;B-vendor/symfony/console/Helper/ProgressBar.php,%:],%X4ö3vendor/symfony/console/Helper/ProgressIndicator.phpE:]E0vendor/symfony/console/Helper/QuestionHelper.php$:]$m7vendor/symfony/console/Helper/SymfonyQuestionHelper.phpV :]V Fo'vendor/symfony/console/Helper/Table.php-:]-š+vendor/symfony/console/Helper/TableCell.phpw:]wV0vendor/symfony/console/Helper/TableSeparator.php:],vendor/symfony/console/Helper/TableStyle.php :] ?0*vendor/symfony/console/Input/ArgvInput.php:]Mo&+vendor/symfony/console/Input/ArrayInput.php :] W.&vendor/symfony/console/Input/Input.phpG :]G .vendor/symfony/console/Input/InputArgument.php :] ;4vendor/symfony/console/Input/InputAwareInterface.php:]O0vendor/symfony/console/Input/InputDefinition.php:]B"퀶/vendor/symfony/console/Input/InputInterface.php:]rrg,vendor/symfony/console/Input/InputOption.php :] 9vendor/symfony/console/Input/StreamableInputInterface.php:]B,vendor/symfony/console/Input/StringInput.phpV:]V.Ķ/vendor/symfony/console/Logger/ConsoleLogger.php :] r0vendor/symfony/console/Output/BufferedOutput.php_:]_>P/vendor/symfony/console/Output/ConsoleOutput.php:]8vendor/symfony/console/Output/ConsoleOutputInterface.php:]ʶ,vendor/symfony/console/Output/NullOutput.php:]Z(vendor/symfony/console/Output/Output.php :] 0p1vendor/symfony/console/Output/OutputInterface.php:]n .vendor/symfony/console/Output/StreamOutput.php:]ĒO2vendor/symfony/console/Question/ChoiceQuestion.phpi :]i =8vendor/symfony/console/Question/ConfirmationQuestion.php:]d?,vendor/symfony/console/Question/Question.php :] ,vendor/symfony/console/Style/OutputStyle.phpW:]W?/vendor/symfony/console/Style/StyleInterface.php:]&nѶ-vendor/symfony/console/Style/SymfonyStyle.php2:]2zc#vendor/symfony/console/Terminal.php:]՝Ŷ3vendor/symfony/console/Tester/ApplicationTester.php' :]' a延/vendor/symfony/console/Tester/CommandTester.php:]4vendor/symfony/css-selector/CssSelectorConverter.php:]G<vendor/symfony/css-selector/Exception/ExceptionInterface.phpm:]mY~Bvendor/symfony/css-selector/Exception/ExpressionErrorException.php:]6;@vendor/symfony/css-selector/Exception/InternalErrorException.php:]&O8vendor/symfony/css-selector/Exception/ParseException.php:]/X>vendor/symfony/css-selector/Exception/SyntaxErrorException.phpo:]ol%1vendor/symfony/css-selector/Node/AbstractNode.phpO:]ORi˶2vendor/symfony/css-selector/Node/AttributeNode.php:].5.vendor/symfony/css-selector/Node/ClassNode.phph:]hңnQ9vendor/symfony/css-selector/Node/CombinedSelectorNode.php:]!a0vendor/symfony/css-selector/Node/ElementNode.php:]}C1vendor/symfony/css-selector/Node/FunctionNode.php:]k y-vendor/symfony/css-selector/Node/HashNode.phpY:]YG1vendor/symfony/css-selector/Node/NegationNode.php:])2vendor/symfony/css-selector/Node/NodeInterface.php:].y/vendor/symfony/css-selector/Node/PseudoNode.php:]g1vendor/symfony/css-selector/Node/SelectorNode.php:])0vendor/symfony/css-selector/Node/Specificity.phps:]sW:O=vendor/symfony/css-selector/Parser/Handler/CommentHandler.php:]!?vendor/symfony/css-selector/Parser/Handler/HandlerInterface.php:]Ki:vendor/symfony/css-selector/Parser/Handler/HashHandler.php:]-Z@vendor/symfony/css-selector/Parser/Handler/IdentifierHandler.php:]ms<vendor/symfony/css-selector/Parser/Handler/NumberHandler.php:]u`Ӷ<vendor/symfony/css-selector/Parser/Handler/StringHandler.php]:]]\@vendor/symfony/css-selector/Parser/Handler/WhitespaceHandler.phpO:]Oah-vendor/symfony/css-selector/Parser/Parser.php":]"36vendor/symfony/css-selector/Parser/ParserInterface.php:]y9-vendor/symfony/css-selector/Parser/Reader.phpE:]ECN;vendor/symfony/css-selector/Parser/Shortcut/ClassParser.phpp:]pH;=vendor/symfony/css-selector/Parser/Shortcut/ElementParser.php :] V!Avendor/symfony/css-selector/Parser/Shortcut/EmptyStringParser.php:] X:vendor/symfony/css-selector/Parser/Shortcut/HashParser.phpl:]lw,vendor/symfony/css-selector/Parser/Token.php:]rL:vendor/symfony/css-selector/Parser/Tokenizer/Tokenizer.php:]JeVBvendor/symfony/css-selector/Parser/Tokenizer/TokenizerEscaping.php:]3XBvendor/symfony/css-selector/Parser/Tokenizer/TokenizerPatterns.php:]J2vendor/symfony/css-selector/Parser/TokenStream.php:]śڞAvendor/symfony/css-selector/XPath/Extension/AbstractExtension.php:]/Jvendor/symfony/css-selector/XPath/Extension/AttributeMatchingExtension.php :] qODvendor/symfony/css-selector/XPath/Extension/CombinationExtension.phpT:]T-жBvendor/symfony/css-selector/XPath/Extension/ExtensionInterface.php:]#.ɶAvendor/symfony/css-selector/XPath/Extension/FunctionExtension.php :] Bǎ=vendor/symfony/css-selector/XPath/Extension/HtmlExtension.php9:]9-^!=vendor/symfony/css-selector/XPath/Extension/NodeExtension.php:]XDvendor/symfony/css-selector/XPath/Extension/PseudoClassExtension.phpN:]N z0vendor/symfony/css-selector/XPath/Translator.php9:]94E9vendor/symfony/css-selector/XPath/TranslatorInterface.phpV:]V[/vendor/symfony/css-selector/XPath/XPathExpr.phpk:]kd0}(vendor/symfony/debug/BufferingLogger.php`:]`Dm۶vendor/symfony/debug/Debug.php+:]+9b)vendor/symfony/debug/DebugClassLoader.php/+:]/+(4%vendor/symfony/debug/ErrorHandler.phpA:]Â9vendor/symfony/debug/Exception/ClassNotFoundException.php:]i8vendor/symfony/debug/Exception/ContextErrorException.php:]^@6vendor/symfony/debug/Exception/FatalErrorException.php.:].J06vendor/symfony/debug/Exception/FatalThrowableError.phpW:]W?K3vendor/symfony/debug/Exception/FlattenException.phpn:]nw 7vendor/symfony/debug/Exception/OutOfMemoryException.php~:]~o7vendor/symfony/debug/Exception/SilencedErrorContext.php(:](=vendor/symfony/debug/Exception/UndefinedFunctionException.php:]J;vendor/symfony/debug/Exception/UndefinedMethodException.php:]nض)vendor/symfony/debug/ExceptionHandler.phpwM:]wM7ĶIvendor/symfony/debug/FatalErrorHandler/ClassNotFoundFatalErrorHandler.phpT:]TrEvendor/symfony/debug/FatalErrorHandler/FatalErrorHandlerInterface.php:]ĹBVMvendor/symfony/debug/FatalErrorHandler/UndefinedFunctionFatalErrorHandler.phps:]skնKvendor/symfony/debug/FatalErrorHandler/UndefinedMethodFatalErrorHandler.phpJ:]J1vendor/symfony/dom-crawler/AbstractUriElement.php :] S&vendor/symfony/dom-crawler/Crawler.phpB:]Brx4vendor/symfony/dom-crawler/Field/ChoiceFormField.php:]( 2vendor/symfony/dom-crawler/Field/FileFormField.php :] Ue.vendor/symfony/dom-crawler/Field/FormField.php:]3vendor/symfony/dom-crawler/Field/InputFormField.php:]!O6vendor/symfony/dom-crawler/Field/TextareaFormField.php:]4^#vendor/symfony/dom-crawler/Form.php):])ǐ0vendor/symfony/dom-crawler/FormFieldRegistry.php+ :]+ Rg$vendor/symfony/dom-crawler/Image.php:]Jѓ#vendor/symfony/dom-crawler/Link.php:]vۮAvendor/symfony/event-dispatcher/ContainerAwareEventDispatcher.php}:]}mvBvendor/symfony/event-dispatcher/Debug/TraceableEventDispatcher.php:]JAS:Kvendor/symfony/event-dispatcher/Debug/TraceableEventDispatcherInterface.php5:]5fV9vendor/symfony/event-dispatcher/Debug/WrappedListener.php :] !Mvendor/symfony/event-dispatcher/DependencyInjection/RegisterListenersPass.php:]\])vendor/symfony/event-dispatcher/Event.php):])@B3vendor/symfony/event-dispatcher/EventDispatcher.php:]"d<vendor/symfony/event-dispatcher/EventDispatcherInterface.php:]pX<vendor/symfony/event-dispatcher/EventSubscriberInterface.php:]o;y0vendor/symfony/event-dispatcher/GenericEvent.phpl:]l@F^Ķ<vendor/symfony/event-dispatcher/ImmutableEventDispatcher.phpq:]q/vendor/symfony/finder/Comparator/Comparator.php:]3vendor/symfony/finder/Comparator/DateComparator.php#:]#ζ5vendor/symfony/finder/Comparator/NumberComparator.php|:]|ITͶ9vendor/symfony/finder/Exception/AccessDeniedException.php:]s6vendor/symfony/finder/Exception/ExceptionInterface.php:]L~ vendor/symfony/finder/Finder.php!:]!Z2|vendor/symfony/finder/Glob.php:]t'7vendor/symfony/finder/Iterator/CustomFilterIterator.phpX:]XDVSB:vendor/symfony/finder/Iterator/DateRangeFilterIterator.phps:]s=x;vendor/symfony/finder/Iterator/DepthRangeFilterIterator.php:]͝qAvendor/symfony/finder/Iterator/ExcludeDirectoryFilterIterator.php:]f_<vendor/symfony/finder/Iterator/FilecontentFilterIterator.php5:]59vendor/symfony/finder/Iterator/FilenameFilterIterator.phpr:]rtu9vendor/symfony/finder/Iterator/FileTypeFilterIterator.phpZ:]Z(&1vendor/symfony/finder/Iterator/FilterIterator.php:] =vendor/symfony/finder/Iterator/MultiplePcreFilterIterator.php :] ƺ5vendor/symfony/finder/Iterator/PathFilterIterator.php:]c=vendor/symfony/finder/Iterator/RecursiveDirectoryIterator.php :] ;+>:vendor/symfony/finder/Iterator/SizeRangeFilterIterator.php`:]`!ky3vendor/symfony/finder/Iterator/SortableIterator.php+:]+.3@%vendor/symfony/finder/SplFileInfo.php:]+vendor/symfony/polyfill-ctype/bootstrap.phpI:]ID!e'vendor/symfony/polyfill-ctype/Ctype.phpH :]H ݶ.vendor/symfony/polyfill-mbstring/bootstrap.php:]<ˢ-vendor/symfony/polyfill-mbstring/Mbstring.phpC:]CZ?@vendor/symfony/polyfill-mbstring/Resources/unidata/lowerCase.php@:]@ضFvendor/symfony/polyfill-mbstring/Resources/unidata/titleCaseRegexp.php:]y_@vendor/symfony/polyfill-mbstring/Resources/unidata/upperCase.phpfA:]fAf߶7vendor/symfony/process/Exception/ExceptionInterface.phpf:]f]>T=vendor/symfony/process/Exception/InvalidArgumentException.php:]+_3vendor/symfony/process/Exception/LogicException.php:] ;vendor/symfony/process/Exception/ProcessFailedException.phpx:]xzy=vendor/symfony/process/Exception/ProcessTimedOutException.php:]5vendor/symfony/process/Exception/RuntimeException.php:]:+vendor/symfony/process/ExecutableFinder.php:]OqH&vendor/symfony/process/InputStream.php$:]$m.vendor/symfony/process/PhpExecutableFinder.php:])Ķ%vendor/symfony/process/PhpProcess.php:]xp.vendor/symfony/process/Pipes/AbstractPipes.php :] /vendor/symfony/process/Pipes/PipesInterface.phpm:]mu*vendor/symfony/process/Pipes/UnixPipes.php:]#̶-vendor/symfony/process/Pipes/WindowsPipes.php :] mN"vendor/symfony/process/Process.phpk:]kNt)vendor/symfony/process/ProcessBuilder.php :] AeѶ'vendor/symfony/process/ProcessUtils.php5:]5nV8+vendor/symfony/yaml/Command/LintCommand.php:]5wvendor/symfony/yaml/Dumper.php :] 4m_vendor/symfony/yaml/Escaper.phpP:]PSz/vendor/symfony/yaml/Exception/DumpException.phps:]sƶ4vendor/symfony/yaml/Exception/ExceptionInterface.phpc:]c۶0vendor/symfony/yaml/Exception/ParseException.phpB:]BH]I2vendor/symfony/yaml/Exception/RuntimeException.php:]$vendor/symfony/yaml/Inline.phpZ:]ZQvendor/symfony/yaml/Parser.phpoj:]ojL'vendor/symfony/yaml/Tag/TaggedValue.php>:]>?!vendor/symfony/yaml/Unescaper.php:]&%vendor/symfony/yaml/Yaml.php :] ?W&vendor/vlucas/phpdotenv/src/Dotenv.php:] =vendor/vlucas/phpdotenv/src/Environment/AbstractVariables.php:]3Dvendor/vlucas/phpdotenv/src/Environment/Adapter/AdapterInterface.php:]ժAvendor/vlucas/phpdotenv/src/Environment/Adapter/ApacheAdapter.php:]M䊶@vendor/vlucas/phpdotenv/src/Environment/Adapter/ArrayAdapter.php:]XGCvendor/vlucas/phpdotenv/src/Environment/Adapter/EnvConstAdapter.php:]sUAvendor/vlucas/phpdotenv/src/Environment/Adapter/PutenvAdapter.php:]Fvendor/vlucas/phpdotenv/src/Environment/Adapter/ServerConstAdapter.php:]dĮ9vendor/vlucas/phpdotenv/src/Environment/DotenvFactory.phpH:]Hhe;vendor/vlucas/phpdotenv/src/Environment/DotenvVariables.php:]sֶ<vendor/vlucas/phpdotenv/src/Environment/FactoryInterface.php:]׶>vendor/vlucas/phpdotenv/src/Environment/VariablesInterface.php<:]<y<vendor/vlucas/phpdotenv/src/Exception/ExceptionInterface.phpI:]IO*>vendor/vlucas/phpdotenv/src/Exception/InvalidFileException.php:]5 >vendor/vlucas/phpdotenv/src/Exception/InvalidPathException.php:]O3=vendor/vlucas/phpdotenv/src/Exception/ValidationException.php:]%vendor/vlucas/phpdotenv/src/Lines.php?:]?e>&vendor/vlucas/phpdotenv/src/Loader.phpW :]W lζ&vendor/vlucas/phpdotenv/src/Parser.php :] 6e+vendor/vlucas/phpdotenv/src/Regex/Error.php>:]>+wڶ+vendor/vlucas/phpdotenv/src/Regex/Regex.php:],vendor/vlucas/phpdotenv/src/Regex/Result.php:]F L-vendor/vlucas/phpdotenv/src/Regex/Success.php@:]@ްʶ)vendor/vlucas/phpdotenv/src/Validator.php2:]20F&vendor/webmozart/assert/src/Assert.phpOX:]OX0ٶ autoload.php= :]= xcodecept:]Ushim.php :] Jscenario = $scenario; } /** * @return \Codeception\Scenario */ protected function getScenario() { return $this->scenario; } public function wantToTest($text) { $this->wantTo('test ' . $text); } public function wantTo($text) { $this->scenario->setFeature($text); } public function __call($method, $arguments) { $class = get_class($this); throw new \RuntimeException("Call to undefined method $class::$method"); } /** * Lazy-execution given anonymous function * @param $callable \Closure * @return $this */ public function execute($callable) { $this->scenario->addStep(new Executor($callable, [])); $callable(); return $this; } } readCustomCommandsFromConfig(); } catch (ConfigurationException $e) { if ($e->getCode() === 404) { return; } $this->renderException($e, new ConsoleOutput()); exit(1); } catch (\Exception $e) { $this->renderException($e, new ConsoleOutput()); exit(1); } } /** * Search custom commands and register them. * * @throws ConfigurationException */ protected function readCustomCommandsFromConfig() { $this->getCoreArguments(); // Maybe load outside configfile $config = Configuration::config(); if (empty($config['extensions']['commands'])) { return; } foreach ($config['extensions']['commands'] as $commandClass) { $commandName = $this->getCustomCommandName($commandClass); $this->add(new $commandClass($commandName)); } } /** * Validate and get the name of the command * * @param CustomCommandInterface $commandClass * * @throws ConfigurationException * * @return string */ protected function getCustomCommandName($commandClass) { if (!class_exists($commandClass)) { throw new ConfigurationException("Extension: Command class $commandClass not found"); } $interfaces = class_implements($commandClass); if (!in_array('Codeception\CustomCommandInterface', $interfaces)) { throw new ConfigurationException("Extension: Command {$commandClass} must implement " . "the interface `Codeception\\CustomCommandInterface`"); } return $commandClass::getCommandName(); } /** * To cache Class ArgvInput * * @inheritDoc */ public function run(InputInterface $input = null, OutputInterface $output = null) { if ($input === null) { $input = $this->getCoreArguments(); } if (!ini_get('register_argc_argv')) { throw new ConfigurationException('register_argc_argv must be set to On for running Codeception'); } return parent::run($input, $output); } /** * Add global a --config option. * * @return InputDefinition */ protected function getDefaultInputDefinition() { $inputDefinition = parent::getDefaultInputDefinition(); $inputDefinition->addOption( new InputOption('config', 'c', InputOption::VALUE_OPTIONAL, 'Use custom path for config') ); return $inputDefinition; } /** * Search for --config Option and if found will be loaded * * example: * -c file.yml|dir * -cfile.yml|dir * --config file.yml|dir * --config=file.yml|dir * * @return ArgvInput */ protected function getCoreArguments() { if ($this->coreArguments !== null) { return $this->coreArguments; } $argvWithoutConfig = []; if (isset($_SERVER['argv'])) { $argv = $_SERVER['argv']; for ($i = 0; $i < count($argv); $i++) { if (preg_match('/^(?:-([^c-]*)?c|--config(?:=|$))(.*)$/', $argv[$i], $match)) { if (!empty($match[2])) { //same index $this->preloadConfiguration($match[2]); } elseif (isset($argv[$i + 1])) { //next index $this->preloadConfiguration($argv[++$i]); } if (!empty($match[1])) { $argvWithoutConfig[] = "-" . $match[1]; //rest commands } continue; } $argvWithoutConfig[] = $argv[$i]; } } return $this->coreArguments = new ArgvInput($argvWithoutConfig); } /** * Pre load Configuration, the config option is use. * * @param string $configFile Path to Configuration * * @throws ConfigurationException */ protected function preloadConfiguration($configFile) { try { Configuration::config($configFile); } catch (ConfigurationException $e) { if ($e->getCode() == 404) { throw new ConfigurationException("Your configuration file `{$configFile}` could not be found.", 405); } throw $e; } } } false, 'debug' => false, 'steps' => false, 'html' => false, 'xml' => false, 'phpunit-xml' => false, 'no-redirect' => true, 'json' => false, 'tap' => false, 'report' => false, 'colors' => false, 'coverage' => false, 'coverage-xml' => false, 'coverage-html' => false, 'coverage-text' => false, 'coverage-crap4j' => false, 'coverage-phpunit'=> false, 'groups' => null, 'excludeGroups' => null, 'filter' => null, 'env' => null, 'fail-fast' => false, 'ansi' => true, 'verbosity' => 1, 'interactive' => true, 'no-rebuild' => false, 'quiet' => false, ]; protected $config = []; /** * @var array */ protected $extensions = []; public function __construct($options = []) { $this->result = new \PHPUnit\Framework\TestResult; $this->dispatcher = new EventDispatcher(); $this->extensionLoader = new ExtensionLoader($this->dispatcher); $baseOptions = $this->mergeOptions($options); $this->extensionLoader->bootGlobalExtensions($baseOptions); // extensions may override config $this->config = Configuration::config(); $this->options = $this->mergeOptions($options); // options updated from config $this->registerSubscribers(); $this->registerPHPUnitListeners(); $this->registerPrinter(); } /** * Merges given options with default values and current configuration * * @param array $options options * @return array * @throws ConfigurationException */ protected function mergeOptions($options) { $config = Configuration::config(); $baseOptions = array_merge($this->options, $config['settings']); return array_merge($baseOptions, $options); } protected function registerPHPUnitListeners() { $listener = new PHPUnit\Listener($this->dispatcher); $this->result->addListener($listener); } public function registerSubscribers() { // required $this->dispatcher->addSubscriber(new Subscriber\GracefulTermination()); $this->dispatcher->addSubscriber(new Subscriber\ErrorHandler()); $this->dispatcher->addSubscriber(new Subscriber\Dependencies()); $this->dispatcher->addSubscriber(new Subscriber\Bootstrap()); $this->dispatcher->addSubscriber(new Subscriber\PrepareTest()); $this->dispatcher->addSubscriber(new Subscriber\Module()); $this->dispatcher->addSubscriber(new Subscriber\BeforeAfterTest()); // optional if (!$this->options['no-rebuild']) { $this->dispatcher->addSubscriber(new Subscriber\AutoRebuild()); } if (!$this->options['silent']) { $this->dispatcher->addSubscriber(new Subscriber\Console($this->options)); } if ($this->options['fail-fast']) { $this->dispatcher->addSubscriber(new Subscriber\FailFast()); } if ($this->options['coverage']) { $this->dispatcher->addSubscriber(new Coverage\Subscriber\Local($this->options)); $this->dispatcher->addSubscriber(new Coverage\Subscriber\LocalServer($this->options)); $this->dispatcher->addSubscriber(new Coverage\Subscriber\RemoteServer($this->options)); $this->dispatcher->addSubscriber(new Coverage\Subscriber\Printer($this->options)); } $this->dispatcher->addSubscriber($this->extensionLoader); $this->extensionLoader->registerGlobalExtensions(); } public function run($suite, $test = null, array $config = null) { ini_set( 'memory_limit', isset($this->config['settings']['memory_limit']) ? $this->config['settings']['memory_limit'] : '1024M' ); $config = $config ?: Configuration::config(); $settings = Configuration::suiteSettings($suite, $config); $selectedEnvironments = $this->options['env']; $environments = Configuration::suiteEnvironments($suite); if (!$selectedEnvironments or empty($environments)) { $this->runSuite($settings, $suite, $test); return; } foreach (array_unique($selectedEnvironments) as $envList) { $envArray = explode(',', $envList); $config = []; foreach ($envArray as $env) { if (isset($environments[$env])) { $currentEnvironment = isset($config['current_environment']) ? [$config['current_environment']] : []; $config = Configuration::mergeConfigs($config, $environments[$env]); $currentEnvironment[] = $config['current_environment']; $config['current_environment'] = implode(',', $currentEnvironment); } } if (empty($config)) { continue; } $suiteToRun = $suite; if (!empty($envList)) { $suiteToRun .= ' (' . implode(', ', $envArray) . ')'; } $this->runSuite($config, $suiteToRun, $test); } } public function runSuite($settings, $suite, $test = null) { $suiteManager = new SuiteManager($this->dispatcher, $suite, $settings); $suiteManager->initialize(); srand($this->options['seed']); $suiteManager->loadTests($test); srand(); $suiteManager->run($this->runner, $this->result, $this->options); return $this->result; } public static function versionString() { return 'Codeception PHP Testing Framework v' . self::VERSION; } public function printResult() { $result = $this->getResult(); $result->flushListeners(); $printer = $this->runner->getPrinter(); $printer->printResult($result); $this->dispatcher->dispatch(Events::RESULT_PRINT_AFTER, new Event\PrintResultEvent($result, $printer)); } /** * @return \PHPUnit\Framework\TestResult */ public function getResult() { return $this->result; } public function getOptions() { return $this->options; } /** * @return EventDispatcher */ public function getDispatcher() { return $this->dispatcher; } protected function registerPrinter() { $printer = new PHPUnit\ResultPrinter\UI($this->dispatcher, $this->options); $this->runner = new PHPUnit\Runner(); $this->runner->setPrinter($printer); } } setDefinition( [ new InputArgument('path', InputArgument::OPTIONAL, 'custom installation dir', null), new InputOption( 'namespace', 's', InputOption::VALUE_OPTIONAL, 'Namespace to add for actor classes and helpers' ), new InputOption('actor', 'a', InputOption::VALUE_OPTIONAL, 'Custom actor instead of Tester'), new InputOption('empty', 'e', InputOption::VALUE_NONE, 'Don\'t create standard suites') ] ); } public function getDescription() { return "Creates default test suites and generates all required files"; } public function execute(InputInterface $input, OutputInterface $output) { $bootstrap = new BootstrapTemplate($input, $output); if ($input->getArgument('path')) { $bootstrap->initDir($input->getArgument('path')); } $bootstrap->setup(); } } output = $output; $this->buildActorsForConfig(); } private function buildActor(array $settings) { $actorGenerator = new ActorGenerator($settings); $this->output->writeln( '' . Configuration::config()['namespace'] . '\\' . $actorGenerator->getActorName() . " includes modules: " . implode(', ', $actorGenerator->getModules()) ); $content = $actorGenerator->produce(); $file = $this->createDirectoryFor( Configuration::supportDir(), $settings['actor'] ) . $this->getShortClassName($settings['actor']); $file .= '.php'; return $this->createFile($file, $content); } private function buildActions(array $settings) { $actionsGenerator = new ActionsGenerator($settings); $content = $actionsGenerator->produce(); $this->output->writeln( " -> {$settings['actor']}Actions.php generated successfully. " . $actionsGenerator->getNumMethods() . " methods added" ); $file = $this->createDirectoryFor(Configuration::supportDir() . '_generated', $settings['actor']); $file .= $this->getShortClassName($settings['actor']) . 'Actions.php'; return $this->createFile($file, $content, true); } private function buildSuiteActors() { $suites = $this->getSuites(); if (!empty($suites)) { $this->output->writeln("Building Actor classes for suites: " . implode(', ', $suites) . ''); } foreach ($suites as $suite) { $settings = $this->getSuiteConfig($suite); if (!$settings['actor']) { continue; // no actor } $this->buildActions($settings); $actorBuilt = $this->buildActor($settings); if ($actorBuilt) { $this->output->writeln("{$settings['actor']}.php created."); } } } protected function buildActorsForConfig($configFile = null) { $config = $this->getGlobalConfig($configFile); $dir = Configuration::projectDir(); $this->buildSuiteActors(); foreach ($config['include'] as $subConfig) { $this->output->writeln("\nIncluded Configuration: $subConfig"); $this->buildActorsForConfig($dir . DIRECTORY_SEPARATOR . $subConfig); } } } cleanProjectsRecursively($output, $projectDir); $output->writeln("Done"); } private function cleanProjectsRecursively(OutputInterface $output, $projectDir) { $logDir = Configuration::logDir(); $output->writeln("Cleaning up output " . $logDir . "..."); FileSystem::doEmptyDir($logDir); $config = Configuration::config($projectDir); $subProjects = $config['include']; foreach ($subProjects as $subProject) { $subProjectDir = $projectDir . $subProject; $this->cleanProjectsRecursively($output, $subProjectDir); } } } addHandler(new ConsoleCompletion( $suiteCommand, 'suite', ConsoleCompletion::TYPE_ARGUMENT, Configuration::suites() )); } $handler->addHandlers([ new ShellPathCompletion( ConsoleCompletion::ALL_COMMANDS, 'path', ConsoleCompletion::TYPE_ARGUMENT ), new ShellPathCompletion( ConsoleCompletion::ALL_COMMANDS, 'test', ConsoleCompletion::TYPE_ARGUMENT ), ]); } protected function execute(InputInterface $input, OutputInterface $output) { if ($input->getOption('generate-hook') && $input->getOption('use-vendor-bin')) { global $argv; $argv[0] = 'vendor/bin/' . basename($argv[0]); } parent::execute($input, $output); } protected function createDefinition() { $definition = parent::createDefinition(); $definition->addOption(new InputOption( 'use-vendor-bin', null, InputOption::VALUE_NONE, 'Use the vendor bin for autocompletion.' )); return $definition; } } setName('_completion') ->setDescription('BASH completion hook.') ->setHelp(<<composer require stecman/symfony-console-completion END ); // Hide this command from listing if supported // Command::setHidden() was not available before Symfony 3.2.0 if (method_exists($this, 'setHidden')) { $this->setHidden(true); } } protected function execute(InputInterface $input, OutputInterface $output) { $output->writeln("Install optional stecman/symfony-console-completion"); } } setDefinition( [ new InputArgument('suite', InputArgument::OPTIONAL, 'to show suite configuration'), new InputOption('config', 'c', InputOption::VALUE_OPTIONAL, 'Use custom path for config'), new InputOption('override', 'o', InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, 'Override config values'), ] ); parent::configure(); } public function getDescription() { return 'Validates and prints config to screen'; } public function execute(InputInterface $input, OutputInterface $output) { $this->addStyles($output); if ($suite = $input->getArgument('suite')) { $output->write("Validating $suite config... "); $config = $this->getSuiteConfig($suite); $output->writeln("Ok"); $output->writeln("------------------------------\n"); $output->writeln("$suite Suite Config:\n"); $output->writeln($this->formatOutput($config)); return; } $output->write("Validating global config... "); $config = $this->getGlobalConfig(); $output->writeln($input->getOption('override')); if (count($input->getOption('override'))) { $config = $this->overrideConfig($input->getOption('override')); } $suites = Configuration::suites(); $output->writeln("Ok"); $output->writeln("------------------------------\n"); $output->writeln("Codeception Config:\n"); $output->writeln($this->formatOutput($config)); $output->writeln('Directories:'); $output->writeln("codecept_root_dir() " . codecept_root_dir()); $output->writeln("codecept_output_dir() " . codecept_output_dir()); $output->writeln("codecept_data_dir() " . codecept_data_dir()); $output->writeln(''); $output->writeln("Available suites: " . implode(', ', $suites)); foreach ($suites as $suite) { $output->write("Validating suite $suite... "); $this->getSuiteConfig($suite); $output->writeln('Ok'); } $output->writeln("Execute codecept config:validate [] to see config for a suite"); } protected function formatOutput($config) { $output = print_r($config, true); return preg_replace('~\[(.*?)\] =>~', "$1 =>", $output); } } setDefinition([ new InputArgument('suite', InputArgument::REQUIRED, 'suite to be executed'), new InputOption('colors', '', InputOption::VALUE_NONE, 'Use colors in output'), ]); parent::configure(); } public function getDescription() { return 'Launches interactive test console'; } public function execute(InputInterface $input, OutputInterface $output) { $suiteName = $input->getArgument('suite'); $this->output = $output; $config = Configuration::config(); $settings = Configuration::suiteSettings($suiteName, $config); $options = $input->getOptions(); $options['debug'] = true; $options['silent'] = true; $options['interactive'] = false; $options['colors'] = true; Debug::setOutput(new Output($options)); $this->codecept = new Codecept($options); $dispatcher = $this->codecept->getDispatcher(); $suiteManager = new SuiteManager($dispatcher, $suiteName, $settings); $suiteManager->initialize(); $this->suite = $suiteManager->getSuite(); $moduleContainer = $suiteManager->getModuleContainer(); $this->actions = array_keys($moduleContainer->getActions()); $this->test = new Cept(null, null); $this->test->getMetadata()->setServices([ 'dispatcher' => $dispatcher, 'modules' => $moduleContainer ]); $scenario = new Scenario($this->test); if (!$settings['actor']) { throw new ConfigurationException("Interactive shell can't be started without an actor"); } if (isset($config["namespace"])) { $settings['actor'] = $config["namespace"] .'\\' . $settings['actor']; } $actor = $settings['actor']; $I = new $actor($scenario); $this->listenToSignals(); $output->writeln("Interactive console started for suite $suiteName"); $output->writeln("Try Codeception commands without writing a test"); $suiteEvent = new SuiteEvent($this->suite, $this->codecept->getResult(), $settings); $dispatcher->dispatch(Events::SUITE_BEFORE, $suiteEvent); $dispatcher->dispatch(Events::TEST_PARSED, new TestEvent($this->test)); $dispatcher->dispatch(Events::TEST_BEFORE, new TestEvent($this->test)); if (file_exists($settings['bootstrap'])) { require $settings['bootstrap']; } $I->pause(); $dispatcher->dispatch(Events::TEST_AFTER, new TestEvent($this->test)); $dispatcher->dispatch(Events::SUITE_AFTER, new SuiteEvent($this->suite)); $output->writeln("Bye-bye!"); } protected function listenToSignals() { if (function_exists('pcntl_signal')) { declare (ticks = 1); pcntl_signal(SIGINT, SIG_IGN); pcntl_signal(SIGTERM, SIG_IGN); } } } setDefinition( [ new InputArgument('suite', InputArgument::REQUIRED, 'suite to scan for feature files'), new InputArgument('test', InputArgument::OPTIONAL, 'tests to be loaded'), ] ); parent::configure(); } public function getDescription() { return 'Prints step-by-step scenario-driven test or a feature'; } public function execute(InputInterface $input, OutputInterface $output) { $this->addStyles($output); $suite = $input->getArgument('suite'); $test = $input->getArgument('test'); $config = $this->getGlobalConfig(); ini_set( 'memory_limit', isset($config['settings']['memory_limit']) ? $config['settings']['memory_limit'] : '1024M' ); if (! Configuration::isEmpty() && ! $test && strpos($suite, $config['paths']['tests']) === 0) { list(, $suite, $test) = $this->matchTestFromFilename($suite, $config['paths']['tests']); } $settings = $this->getSuiteConfig($suite); $dispatcher = new EventDispatcher(); $dispatcher->addSubscriber(new ConsolePrinter([ 'colors' => !$input->getOption('no-ansi'), 'steps' => true, 'verbosity' => OutputInterface::VERBOSITY_VERBOSE, ])); $dispatcher->addSubscriber(new BootstrapLoader()); $suiteManager = new SuiteManager($dispatcher, $suite, $settings); $moduleContainer = $suiteManager->getModuleContainer(); foreach (Configuration::modules($settings) as $module) { $moduleContainer->mock($module, new Maybe()); } $suiteManager->loadTests($test); $tests = $suiteManager->getSuite()->tests(); $dispatcher->dispatch(Events::SUITE_INIT, new SuiteEvent($suiteManager->getSuite(), null, $settings)); $dispatcher->dispatch(Events::SUITE_BEFORE, new SuiteEvent($suiteManager->getSuite(), null, $settings)); foreach ($tests as $test) { if ($test instanceof \PHPUnit\Framework\TestSuite\DataProvider) { foreach ($test as $t) { if ($t instanceof Test) { $this->dryRunTest($output, $dispatcher, $t); } } } if ($test instanceof Test and $test instanceof ScenarioDriven) { $this->dryRunTest($output, $dispatcher, $test); } } $dispatcher->dispatch(Events::SUITE_AFTER, new SuiteEvent($suiteManager->getSuite())); } protected function matchTestFromFilename($filename, $tests_path) { $filename = str_replace(['//', '\/', '\\'], '/', $filename); $res = preg_match("~^$tests_path/(.*?)/(.*)$~", $filename, $matches); if (!$res) { throw new \InvalidArgumentException("Test file can't be matched"); } return $matches; } /** * @param OutputInterface $output * @param $dispatcher * @param $test */ protected function dryRunTest(OutputInterface $output, EventDispatcher $dispatcher, Test $test) { $dispatcher->dispatch(Events::TEST_START, new TestEvent($test)); $dispatcher->dispatch(Events::TEST_BEFORE, new TestEvent($test)); try { $test->test(); } catch (\Exception $e) { } $dispatcher->dispatch(Events::TEST_AFTER, new TestEvent($test)); $dispatcher->dispatch(Events::TEST_END, new TestEvent($test)); if ($test->getMetadata()->isBlocked()) { $output->writeln(''); if ($skip = $test->getMetadata()->getSkip()) { $output->writeln(" SKIPPED " . $skip); } if ($incomplete = $test->getMetadata()->getIncomplete()) { $output->writeln(" INCOMPLETE " . $incomplete); } } $output->writeln(''); } } setDefinition([ new InputArgument('suite', InputArgument::REQUIRED, 'suite to be tested'), new InputArgument('test', InputArgument::REQUIRED, 'test to be run'), ]); } public function getDescription() { return 'Generates empty Cept file in suite'; } public function execute(InputInterface $input, OutputInterface $output) { $suite = $input->getArgument('suite'); $filename = $input->getArgument('test'); $config = $this->getSuiteConfig($suite); $this->createDirectoryFor($config['path'], $filename); $filename = $this->completeSuffix($filename, 'Cept'); $gen = new Cept($config); $full_path = rtrim($config['path'], DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $filename; $res = $this->createFile($full_path, $gen->produce()); if (!$res) { $output->writeln("Test $filename already exists"); return; } $output->writeln("Test was created in $full_path"); } } setDefinition([ new InputArgument('suite', InputArgument::REQUIRED, 'suite where tests will be put'), new InputArgument('class', InputArgument::REQUIRED, 'test name'), ]); } public function getDescription() { return 'Generates empty Cest file in suite'; } public function execute(InputInterface $input, OutputInterface $output) { $suite = $input->getArgument('suite'); $class = $input->getArgument('class'); $config = $this->getSuiteConfig($suite); $className = $this->getShortClassName($class); $path = $this->createDirectoryFor($config['path'], $class); $filename = $this->completeSuffix($className, 'Cest'); $filename = $path . $filename; if (file_exists($filename)) { $output->writeln("Test $filename already exists"); return; } $gen = new CestGenerator($class, $config); $res = $this->createFile($filename, $gen->produce()); if (!$res) { $output->writeln("Test $filename already exists"); return; } $output->writeln("Test was created in $filename"); } } setDefinition([ new InputArgument('env', InputArgument::REQUIRED, 'Environment name'), ]); } public function getDescription() { return 'Generates empty environment config'; } public function execute(InputInterface $input, OutputInterface $output) { $conf = $this->getGlobalConfig(); if (!Configuration::envsDir()) { throw new ConfigurationException( "Path for environments configuration is not set.\n" . "Please specify envs path in your `codeception.yml`\n \n" . "envs: tests/_envs" ); } $relativePath = $conf['paths']['envs']; $env = $input->getArgument('env'); $file = "$env.yml"; $path = $this->createDirectoryFor($relativePath, $file); $saved = $this->createFile($path . $file, "# `$env` environment config goes here"); if ($saved) { $output->writeln("$env config was created in $relativePath/$file"); } else { $output->writeln("File $relativePath/$file already exists"); } } } setDefinition([ new InputArgument('suite', InputArgument::REQUIRED, 'suite to be tested'), new InputArgument('feature', InputArgument::REQUIRED, 'feature to be generated'), new InputOption('config', 'c', InputOption::VALUE_OPTIONAL, 'Use custom path for config'), ]); } public function getDescription() { return 'Generates empty feature file in suite'; } public function execute(InputInterface $input, OutputInterface $output) { $suite = $input->getArgument('suite'); $filename = $input->getArgument('feature'); $config = $this->getSuiteConfig($suite); $this->createDirectoryFor($config['path'], $filename); $gen = new Feature(basename($filename)); if (!preg_match('~\.feature$~', $filename)) { $filename .= '.feature'; } $full_path = rtrim($config['path'], DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $filename; $res = $this->createFile($full_path, $gen->produce()); if (!$res) { $output->writeln("Feature $filename already exists"); return; } $output->writeln("Feature was created in $full_path"); } } setDefinition([ new InputArgument('group', InputArgument::REQUIRED, 'Group class name'), ]); } public function getDescription() { return 'Generates Group subscriber'; } public function execute(InputInterface $input, OutputInterface $output) { $config = $this->getGlobalConfig(); $group = $input->getArgument('group'); $class = ucfirst($group); $path = $this->createDirectoryFor(Configuration::supportDir() . 'Group' . DIRECTORY_SEPARATOR, $class); $filename = $path . $class . '.php'; $gen = new GroupGenerator($config, $group); $res = $this->createFile($filename, $gen->produce()); if (!$res) { $output->writeln("Group $filename already exists"); return; } $output->writeln("Group extension was created in $filename"); $output->writeln( 'To use this group extension, include it to "extensions" option of global Codeception config.' ); } } setDefinition([ new InputArgument('name', InputArgument::REQUIRED, 'helper name'), ]); } public function getDescription() { return 'Generates new helper'; } public function execute(InputInterface $input, OutputInterface $output) { $name = ucfirst($input->getArgument('name')); $config = $this->getGlobalConfig(); $path = $this->createDirectoryFor(Configuration::supportDir() . 'Helper', $name); $filename = $path . $this->getShortClassName($name) . '.php'; $res = $this->createFile($filename, (new Helper($name, $config['namespace']))->produce()); if ($res) { $output->writeln("Helper $filename created"); } else { $output->writeln("Error creating helper $filename"); } } } setDefinition([ new InputArgument('suite', InputArgument::REQUIRED, 'Either suite name or page object name)'), new InputArgument('page', InputArgument::OPTIONAL, 'Page name of pageobject to represent'), ]); parent::configure(); } public function getDescription() { return 'Generates empty PageObject class'; } public function execute(InputInterface $input, OutputInterface $output) { $suite = $input->getArgument('suite'); $class = $input->getArgument('page'); if (!$class) { $class = $suite; $suite = null; } $conf = $suite ? $this->getSuiteConfig($suite) : $this->getGlobalConfig(); if ($suite) { $suite = DIRECTORY_SEPARATOR . ucfirst($suite); } $path = $this->createDirectoryFor(Configuration::supportDir() . 'Page' . $suite, $class); $filename = $path . $this->getShortClassName($class) . '.php'; $output->writeln($filename); $gen = new PageObjectGenerator($conf, ucfirst($suite) . '\\' . $class); $res = $this->createFile($filename, $gen->produce()); if (!$res) { $output->writeln("PageObject $filename already exists"); exit; } $output->writeln("PageObject was created in $filename"); } protected function pathToPageObject($class, $suite) { } } setDefinition([ new InputArgument('suite', InputArgument::REQUIRED, 'suite from which texts should be generated'), new InputOption('path', 'p', InputOption::VALUE_REQUIRED, 'Use specified path as destination instead of default'), new InputOption('single-file', '', InputOption::VALUE_NONE, 'Render all scenarios to only one file'), new InputOption('format', 'f', InputOption::VALUE_REQUIRED, 'Specify output format: html or text (default)', 'text'), ]); parent::configure(); } public function getDescription() { return 'Generates text representation for all scenarios'; } protected function execute(InputInterface $input, OutputInterface $output) { $suite = $input->getArgument('suite'); $suiteConf = $this->getSuiteConfig($suite); $path = $input->getOption('path') ? $input->getOption('path') : Configuration::dataDir() . 'scenarios'; $format = $input->getOption('format'); @mkdir($path); if (!is_writable($path)) { throw new ConfigurationException( "Path $path is not writable. Please, set valid permissions for folder to store scenarios." ); } $path = $path . DIRECTORY_SEPARATOR . $suite; if (!$input->getOption('single-file')) { @mkdir($path); } $suiteManager = new \Codeception\SuiteManager(new EventDispatcher(), $suite, $suiteConf); if ($suiteConf['bootstrap']) { if (file_exists($suiteConf['path'] . $suiteConf['bootstrap'])) { require_once $suiteConf['path'] . $suiteConf['bootstrap']; } } $tests = $this->getTests($suiteManager); $scenarios = ""; foreach ($tests as $test) { if (!($test instanceof ScenarioDriven)) { continue; } $feature = $test->getScenarioText($format); $name = $this->underscore(basename($test->getFileName(), '.php')); // create separate file for each test in Cest if ($test instanceof Cest && !$input->getOption('single-file')) { $name .= '.' . $this->underscore($test->getTestMethod()); } if ($input->getOption('single-file')) { $scenarios .= $feature; $output->writeln("* $name rendered"); } else { $feature = $this->decorate($feature, $format); $this->createFile($path . DIRECTORY_SEPARATOR . $name . $this->formatExtension($format), $feature, true); $output->writeln("* $name generated"); } } if ($input->getOption('single-file')) { $this->createFile($path . $this->formatExtension($format), $this->decorate($scenarios, $format), true); } } protected function decorate($text, $format) { switch ($format) { case 'text': return $text; case 'html': return "$text"; } } protected function getTests($suiteManager) { $suiteManager->loadTests(); return $suiteManager->getSuite()->tests(); } protected function formatExtension($format) { switch ($format) { case 'text': return '.txt'; case 'html': return '.html'; } } private function underscore($name) { $name = preg_replace('/([A-Z]+)([A-Z][a-z])/', '\\1_\\2', $name); $name = preg_replace('/([a-z\d])([A-Z])/', '\\1_\\2', $name); $name = str_replace(['/', '\\'], ['.', '.'], $name); $name = preg_replace('/_Cept$/', '', $name); $name = preg_replace('/_Cest$/', '', $name); return $name; } } setDefinition([ new InputArgument('suite', InputArgument::REQUIRED, 'Suite name or snapshot name)'), new InputArgument('snapshot', InputArgument::OPTIONAL, 'Name of snapshot'), ]); parent::configure(); } public function getDescription() { return 'Generates empty Snapshot class'; } public function execute(InputInterface $input, OutputInterface $output) { $suite = $input->getArgument('suite'); $class = $input->getArgument('snapshot'); if (!$class) { $class = $suite; $suite = null; } $conf = $suite ? $this->getSuiteConfig($suite) : $this->getGlobalConfig(); if ($suite) { $suite = DIRECTORY_SEPARATOR . ucfirst($suite); } $path = $this->createDirectoryFor(Configuration::supportDir() . 'Snapshot' . $suite, $class); $filename = $path . $this->getShortClassName($class) . '.php'; $output->writeln($filename); $gen = new SnapshotGenerator($conf, ucfirst($suite) . '\\' . $class); $res = $this->createFile($filename, $gen->produce()); if (!$res) { $output->writeln("Snapshot $filename already exists"); exit; } $output->writeln("Snapshot was created in $filename"); } } setDefinition([ new InputArgument('suite', InputArgument::REQUIRED, 'Suite for StepObject'), new InputArgument('step', InputArgument::REQUIRED, 'StepObject name'), new InputOption('silent', '', InputOption::VALUE_NONE, 'skip verification question'), ]); } public function getDescription() { return 'Generates empty StepObject class'; } public function execute(InputInterface $input, OutputInterface $output) { $suite = $input->getArgument('suite'); $step = $input->getArgument('step'); $config = $this->getSuiteConfig($suite); $class = $this->getShortClassName($step); $path = $this->createDirectoryFor(Configuration::supportDir() . 'Step' . DIRECTORY_SEPARATOR . ucfirst($suite), $step); $dialog = $this->getHelperSet()->get('question'); $filename = $path . $class . '.php'; $helper = $this->getHelper('question'); $question = new Question("Add action to StepObject class (ENTER to exit): "); $gen = new StepObjectGenerator($config, ucfirst($suite) . '\\' . $step); if (!$input->getOption('silent')) { do { $question = new Question('Add action to StepObject class (ENTER to exit): ', null); $action = $dialog->ask($input, $output, $question); if ($action) { $gen->createAction($action); } } while ($action); } $res = $this->createFile($filename, $gen->produce()); if (!$res) { $output->writeln("StepObject $filename already exists"); exit; } $output->writeln("StepObject was created in $filename"); } } api + ApiTester * * `codecept g:suite integration Code` -> integration + CodeTester * * `codecept g:suite frontend Front` -> frontend + FrontTester * */ class GenerateSuite extends Command { use Shared\FileSystem; use Shared\Config; use Shared\Style; protected function configure() { $this->setDefinition([ new InputArgument('suite', InputArgument::REQUIRED, 'suite to be generated'), new InputArgument('actor', InputArgument::OPTIONAL, 'name of new actor class'), ]); } public function getDescription() { return 'Generates new test suite'; } public function execute(InputInterface $input, OutputInterface $output) { $this->addStyles($output); $suite = $input->getArgument('suite'); $actor = $input->getArgument('actor'); if ($this->containsInvalidCharacters($suite)) { $output->writeln("Suite name '$suite' contains invalid characters. ([A-Za-z0-9_])."); return; } $config = $this->getGlobalConfig(); if (!$actor) { $actor = ucfirst($suite) . $config['actor_suffix']; } $dir = Configuration::testsDir(); if (file_exists($dir . $suite . '.suite.yml')) { throw new \Exception("Suite configuration file '$suite.suite.yml' already exists."); } $this->createDirectoryFor($dir . $suite); if ($config['settings']['bootstrap']) { // generate bootstrap file $this->createFile( $dir . $suite . DIRECTORY_SEPARATOR . $config['settings']['bootstrap'], "createDirectoryFor( Configuration::supportDir() . "Helper", "$helperName.php" ) . "$helperName.php"; $gen = new Helper($helperName, $config['namespace']); // generate helper $this->createFile( $file, $gen->produce() ); $output->writeln("Helper " . $gen->getHelperName() . " was created in $file"); $yamlSuiteConfigTemplate = <<createFile( $dir . $suite . '.suite.yml', $yamlSuiteConfig = (new Template($yamlSuiteConfigTemplate)) ->place('actor', $actor) ->place('helper', $gen->getHelperName()) ->produce() ); Configuration::append(Yaml::parse($yamlSuiteConfig)); $actorGenerator = new ActorGenerator(Configuration::config()); $content = $actorGenerator->produce(); $file = $this->createDirectoryFor( Configuration::supportDir(), $actor ) . $this->getShortClassName($actor); $file .= '.php'; $this->createFile($file, $content); $output->writeln("Actor " . $actor . " was created in $file"); $output->writeln("Suite config $suite.suite.yml was created."); $output->writeln(' '); $output->writeln("Next steps:"); $output->writeln("1. Edit $suite.suite.yml to enable modules for this suite"); $output->writeln("2. Create first test with generate:cest testName ( or test|cept) command"); $output->writeln("3. Run tests of this suite with codecept run $suite command"); $output->writeln("Suite $suite generated"); } private function containsInvalidCharacters($suite) { return preg_match('#[^A-Za-z0-9_]#', $suite) ? true : false; } } setDefinition( [ new InputArgument('suite', InputArgument::REQUIRED, 'suite where tests will be put'), new InputArgument('class', InputArgument::REQUIRED, 'class name'), ] ); parent::configure(); } public function getDescription() { return 'Generates empty unit test file in suite'; } public function execute(InputInterface $input, OutputInterface $output) { $suite = $input->getArgument('suite'); $class = $input->getArgument('class'); $config = $this->getSuiteConfig($suite); $className = $this->getShortClassName($class); $path = $this->createDirectoryFor($config['path'], $class); $filename = $this->completeSuffix($className, 'Test'); $filename = $path . $filename; $gen = new TestGenerator($config, $class); $res = $this->createFile($filename, $gen->produce()); if (!$res) { $output->writeln("Test $filename already exists"); return; } $output->writeln("Test was created in $filename"); } } setDefinition( [ new InputArgument('suite', InputArgument::REQUIRED, 'suite to scan for feature files'), new InputArgument('test', InputArgument::OPTIONAL, 'test to be scanned'), new InputOption('config', 'c', InputOption::VALUE_OPTIONAL, 'Use custom path for config'), ] ); parent::configure(); } public function getDescription() { return 'Fetches empty steps from feature files of suite and prints code snippets for them'; } public function execute(InputInterface $input, OutputInterface $output) { $this->addStyles($output); $suite = $input->getArgument('suite'); $test = $input->getArgument('test'); $config = $this->getSuiteConfig($suite); $generator = new SnippetsGenerator($config, $test); $snippets = $generator->getSnippets(); $features = $generator->getFeatures(); if (empty($snippets)) { $output->writeln(" All Gherkin steps are defined. Exiting... "); return; } $output->writeln(" Snippets found in: "); foreach ($features as $feature) { $output->writeln(" - {$feature} "); } $output->writeln(" Generated Snippets: "); $output->writeln(" ----------------------------------------- "); foreach ($snippets as $snippet) { $output->writeln($snippet); } $output->writeln(" ----------------------------------------- "); $output->writeln(sprintf(' %d snippets proposed', count($snippets))); $output->writeln(" Copy generated snippets to {$config['actor']} or a specific Gherkin context "); } } setDefinition( [ new InputArgument('suite', InputArgument::REQUIRED, 'suite to scan for feature files'), new InputOption('config', 'c', InputOption::VALUE_OPTIONAL, 'Use custom path for config'), ] ); parent::configure(); } public function getDescription() { return 'Prints all defined feature steps'; } public function execute(InputInterface $input, OutputInterface $output) { $this->addStyles($output); $suite = $input->getArgument('suite'); $config = $this->getSuiteConfig($suite); $config['describe_steps'] = true; $loader = new Gherkin($config); $steps = $loader->getSteps(); foreach ($steps as $name => $context) { /** @var $table Table **/ $table = new Table($output); $table->setHeaders(['Step', 'Implementation']); $output->writeln("Steps from $name context:"); foreach ($context as $step => $callable) { if (count($callable) < 2) { continue; } $method = $callable[0] . '::' . $callable[1]; $table->addRow([$step, $method]); } $table->render(); } if (!isset($table)) { $output->writeln("No steps are defined, start creating them by running gherkin:snippets"); } } } setDefinition( [ new InputArgument('template', InputArgument::REQUIRED, 'Init template for the setup'), new InputOption('path', null, InputOption::VALUE_REQUIRED, 'Change current directory', null), new InputOption('namespace', null, InputOption::VALUE_OPTIONAL, 'Namespace to add for actor classes and helpers\'', null), ] ); } public function getDescription() { return "Creates test suites by a template"; } public function execute(InputInterface $input, OutputInterface $output) { $template = $input->getArgument('template'); if (class_exists($template)) { $className = $template; } else { $className = 'Codeception\Template\\' . ucfirst($template); if (!class_exists($className)) { throw new \Exception("Template from a $className can't be loaded; Init can't be executed"); } } $initProcess = new $className($input, $output); if (!$initProcess instanceof InitTemplate) { throw new \Exception("$className is not a valid template"); } if ($input->getOption('path')) { $initProcess->initDir($input->getOption('path')); } $initProcess->setup(); } } setDefinition([ new InputArgument('suite', InputArgument::OPTIONAL, 'suite to be tested'), new InputArgument('test', InputArgument::OPTIONAL, 'test to be run'), new InputOption('override', 'o', InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, 'Override config values'), new InputOption('ext', 'e', InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, 'Run with extension enabled'), new InputOption('report', '', InputOption::VALUE_NONE, 'Show output in compact style'), new InputOption('html', '', InputOption::VALUE_OPTIONAL, 'Generate html with results', 'report.html'), new InputOption('xml', '', InputOption::VALUE_OPTIONAL, 'Generate JUnit XML Log', 'report.xml'), new InputOption('phpunit-xml', '', InputOption::VALUE_OPTIONAL, 'Generate PhpUnit XML Log', 'phpunit-report.xml'), new InputOption('tap', '', InputOption::VALUE_OPTIONAL, 'Generate Tap Log', 'report.tap.log'), new InputOption('json', '', InputOption::VALUE_OPTIONAL, 'Generate Json Log', 'report.json'), new InputOption('colors', '', InputOption::VALUE_NONE, 'Use colors in output'), new InputOption( 'no-colors', '', InputOption::VALUE_NONE, 'Force no colors in output (useful to override config file)' ), new InputOption('silent', '', InputOption::VALUE_NONE, 'Only outputs suite names and final results'), new InputOption('steps', '', InputOption::VALUE_NONE, 'Show steps in output'), new InputOption('debug', 'd', InputOption::VALUE_NONE, 'Show debug and scenario output'), new InputOption('no-redirect', '', InputOption::VALUE_NONE, 'Do not redirect to Composer-installed version in vendor/codeception'), new InputOption( 'coverage', '', InputOption::VALUE_OPTIONAL, 'Run with code coverage' ), new InputOption( 'coverage-html', '', InputOption::VALUE_OPTIONAL, 'Generate CodeCoverage HTML report in path' ), new InputOption( 'coverage-xml', '', InputOption::VALUE_OPTIONAL, 'Generate CodeCoverage XML report in file' ), new InputOption( 'coverage-text', '', InputOption::VALUE_OPTIONAL, 'Generate CodeCoverage text report in file' ), new InputOption( 'coverage-crap4j', '', InputOption::VALUE_OPTIONAL, 'Generate CodeCoverage report in Crap4J XML format' ), new InputOption( 'coverage-phpunit', '', InputOption::VALUE_OPTIONAL, 'Generate CodeCoverage PHPUnit report in path' ), new InputOption('no-exit', '', InputOption::VALUE_NONE, 'Don\'t finish with exit code'), new InputOption( 'group', 'g', InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, 'Groups of tests to be executed' ), new InputOption( 'skip', 's', InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, 'Skip selected suites' ), new InputOption( 'skip-group', 'x', InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, 'Skip selected groups' ), new InputOption( 'env', '', InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, 'Run tests in selected environments.' ), new InputOption('fail-fast', 'f', InputOption::VALUE_NONE, 'Stop after first failure'), new InputOption('no-rebuild', '', InputOption::VALUE_NONE, 'Do not rebuild actor classes on start'), new InputOption( 'seed', '', InputOption::VALUE_REQUIRED, 'Define random seed for shuffle setting' ), ]); parent::configure(); } public function getDescription() { return 'Runs the test suites'; } /** * Executes Run * * @param \Symfony\Component\Console\Input\InputInterface $input * @param \Symfony\Component\Console\Output\OutputInterface $output * @return int|null|void * @throws \RuntimeException */ public function execute(InputInterface $input, OutputInterface $output) { $this->ensurePhpExtIsAvailable('CURL'); $this->ensurePhpExtIsAvailable('mbstring'); $this->options = $input->getOptions(); $this->output = $output; // load config $config = $this->getGlobalConfig(); // update config from options if (count($this->options['override'])) { $config = $this->overrideConfig($this->options['override']); } if ($this->options['ext']) { $config = $this->enableExtensions($this->options['ext']); } if (!$this->options['colors']) { $this->options['colors'] = $config['settings']['colors']; } if (!$this->options['silent']) { $this->output->writeln( Codecept::versionString() . "\nPowered by " . \PHPUnit\Runner\Version::getVersionString() ); $this->output->writeln( "Running with seed: " . $this->options['seed'] . "\n" ); } if ($this->options['debug']) { $this->output->setVerbosity(OutputInterface::VERBOSITY_VERY_VERBOSE); } $userOptions = array_intersect_key($this->options, array_flip($this->passedOptionKeys($input))); $userOptions = array_merge( $userOptions, $this->booleanOptions($input, [ 'xml' => 'report.xml', 'phpunit-xml' => 'phpunit-report.xml', 'html' => 'report.html', 'json' => 'report.json', 'tap' => 'report.tap.log', 'coverage' => 'coverage.serialized', 'coverage-xml' => 'coverage.xml', 'coverage-html' => 'coverage', 'coverage-text' => 'coverage.txt', 'coverage-crap4j' => 'crap4j.xml', 'coverage-phpunit' => 'coverage-phpunit']) ); $userOptions['verbosity'] = $this->output->getVerbosity(); $userOptions['interactive'] = !$input->hasParameterOption(['--no-interaction', '-n']); $userOptions['ansi'] = (!$input->hasParameterOption('--no-ansi') xor $input->hasParameterOption('ansi')); if (!$this->options['seed']) { $userOptions['seed'] = rand(); } else { $userOptions['seed'] = intval($this->options['seed']); } if ($this->options['no-colors'] || !$userOptions['ansi']) { $userOptions['colors'] = false; } if ($this->options['group']) { $userOptions['groups'] = $this->options['group']; } if ($this->options['skip-group']) { $userOptions['excludeGroups'] = $this->options['skip-group']; } if ($this->options['report']) { $userOptions['silent'] = true; } if ($this->options['coverage-xml'] or $this->options['coverage-html'] or $this->options['coverage-text'] or $this->options['coverage-crap4j'] or $this->options['coverage-phpunit']) { $this->options['coverage'] = true; } if (!$userOptions['ansi'] && $input->getOption('colors')) { $userOptions['colors'] = true; // turn on colors even in non-ansi mode if strictly passed } $suite = $input->getArgument('suite'); $test = $input->getArgument('test'); if ($this->options['group']) { $this->output->writeln(sprintf("[Groups] %s ", implode(', ', $this->options['group']))); } if ($input->getArgument('test')) { $this->options['steps'] = true; } if (!$test) { // Check if suite is given and is in an included path if (!empty($suite) && !empty($config['include'])) { $isIncludeTest = false; // Remember original projectDir $projectDir = Configuration::projectDir(); foreach ($config['include'] as $include) { // Find if the suite begins with an include path if (strpos($suite, $include) === 0) { // Use include config $config = Configuration::config($projectDir.$include); if (!isset($config['paths']['tests'])) { throw new \RuntimeException( sprintf("Included '%s' has no tests path configured", $include) ); } $testsPath = $include . DIRECTORY_SEPARATOR. $config['paths']['tests']; try { list(, $suite, $test) = $this->matchTestFromFilename($suite, $testsPath); $isIncludeTest = true; } catch (\InvalidArgumentException $e) { // Incorrect include match, continue trying to find one continue; } } else { $result = $this->matchSingleTest($suite, $config); if ($result) { list(, $suite, $test) = $result; } } } // Restore main config if (!$isIncludeTest) { $config = Configuration::config($projectDir); } } elseif (!empty($suite)) { $result = $this->matchSingleTest($suite, $config); if ($result) { list(, $suite, $test) = $result; } } } if ($test) { $filter = $this->matchFilteredTestName($test); $userOptions['filter'] = $filter; } if (!$this->options['silent'] && $config['settings']['shuffle']) { $this->output->writeln( "[Seed] " . $userOptions['seed'] . "" ); } $this->codecept = new Codecept($userOptions); if ($suite and $test) { $this->codecept->run($suite, $test, $config); } // Run all tests of given suite or all suites if (!$test) { $suites = $suite ? explode(',', $suite) : Configuration::suites(); $this->executed = $this->runSuites($suites, $this->options['skip']); if (!empty($config['include']) and !$suite) { $current_dir = Configuration::projectDir(); $suites += $config['include']; $this->runIncludedSuites($config['include'], $current_dir); } if ($this->executed === 0) { throw new \RuntimeException( sprintf("Suite '%s' could not be found", implode(', ', $suites)) ); } } $this->codecept->printResult(); if (!$input->getOption('no-exit')) { if (!$this->codecept->getResult()->wasSuccessful()) { exit(1); } } } protected function matchSingleTest($suite, $config) { // Workaround when codeception.yml is inside tests directory and tests path is set to "." // @see https://github.com/Codeception/Codeception/issues/4432 if (isset($config['paths']['tests']) && $config['paths']['tests'] === '.' && !preg_match('~^\.[/\\\]~', $suite)) { $suite = './' . $suite; } // running a single test when suite has a configured path if (isset($config['suites'])) { foreach ($config['suites'] as $s => $suiteConfig) { if (!isset($suiteConfig['path'])) { continue; } $testsPath = $config['paths']['tests'] . DIRECTORY_SEPARATOR . $suiteConfig['path']; if ($suiteConfig['path'] === '.') { $testsPath = $config['paths']['tests']; } if (preg_match("~^$testsPath/(.*?)$~", $suite, $matches)) { $matches[2] = $matches[1]; $matches[1] = $s; return $matches; } } } // Run single test without included tests if (! Configuration::isEmpty() && strpos($suite, $config['paths']['tests']) === 0) { return $this->matchTestFromFilename($suite, $config['paths']['tests']); } } /** * Runs included suites recursively * * @param array $suites * @param string $parent_dir */ protected function runIncludedSuites($suites, $parent_dir) { foreach ($suites as $relativePath) { $current_dir = rtrim($parent_dir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $relativePath; $config = Configuration::config($current_dir); $suites = Configuration::suites(); $namespace = $this->currentNamespace(); $this->output->writeln( "\n\n[$namespace]: tests from $current_dir\n" ); $this->executed += $this->runSuites($suites, $this->options['skip']); if (!empty($config['include'])) { $this->runIncludedSuites($config['include'], $current_dir); } } } protected function currentNamespace() { $config = Configuration::config(); if (!$config['namespace']) { throw new \RuntimeException( "Can't include into runner suite without a namespace;\n" . "Please add `namespace` section into included codeception.yml file" ); } return $config['namespace']; } protected function runSuites($suites, $skippedSuites = []) { $executed = 0; foreach ($suites as $suite) { if (in_array($suite, $skippedSuites)) { continue; } if (!in_array($suite, Configuration::suites())) { continue; } $this->codecept->run($suite); $executed++; } return $executed; } protected function matchTestFromFilename($filename, $testsPath) { $testsPath = str_replace(['//', '\/', '\\'], '/', $testsPath); $filename = str_replace(['//', '\/', '\\'], '/', $filename); $res = preg_match("~^$testsPath/(.*?)(?>/(.*))?$~", $filename, $matches); if (!$res) { throw new \InvalidArgumentException("Test file can't be matched"); } if (!isset($matches[2])) { $matches[2] = null; } return $matches; } private function matchFilteredTestName(&$path) { $test_parts = explode(':', $path, 2); if (count($test_parts) > 1) { list($path, $filter) = $test_parts; // use carat to signify start of string like in normal regex // phpunit --filter matches against the fully qualified method name, so tests actually begin with : $carat_pos = strpos($filter, '^'); if ($carat_pos !== false) { $filter = substr_replace($filter, ':', $carat_pos, 1); } return $filter; } return null; } protected function passedOptionKeys(InputInterface $input) { $options = []; $request = (string)$input; $tokens = explode(' ', $request); foreach ($tokens as $token) { $token = preg_replace('~=.*~', '', $token); // strip = from options if (empty($token)) { continue; } if ($token == '--') { break; // there should be no options after ' -- ', only arguments } if (substr($token, 0, 2) === '--') { $options[] = substr($token, 2); } elseif ($token[0] === '-') { $shortOption = substr($token, 1); $options[] = $this->getDefinition()->getOptionForShortcut($shortOption)->getName(); } } return $options; } protected function booleanOptions(InputInterface $input, $options = []) { $values = []; $request = (string)$input; foreach ($options as $option => $defaultValue) { if (strpos($request, "--$option")) { $values[$option] = $input->getOption($option) ? $input->getOption($option) : $defaultValue; } else { $values[$option] = false; } } return $values; } /** * @param string $ext * @throws \Exception */ private function ensurePhpExtIsAvailable($ext) { if (!extension_loaded(strtolower($ext))) { throw new \Exception( "Codeception requires \"{$ext}\" extension installed to make tests run\n" . "If you are not sure, how to install \"{$ext}\", please refer to StackOverflow\n\n" . "Notice: PHP for Apache/Nginx and CLI can have different php.ini files.\n" . "Please make sure that your PHP you run from console has \"{$ext}\" enabled." ); } } } */ class SelfUpdate extends Command { /** * Class constants */ const NAME = 'Codeception'; const GITHUB_REPO = 'Codeception/Codeception'; const PHAR_URL = 'http://codeception.com/releases/%s/codecept.phar'; const PHAR_URL_PHP54 = 'http://codeception.com/releases/%s/php54/codecept.phar'; /** * Holds the current script filename. * @var string */ protected $filename; /** * Holds the live version string. * @var string */ protected $liveVersion; /** * {@inheritdoc} */ protected function configure() { if (isset($_SERVER['argv'], $_SERVER['argv'][0])) { $this->filename = $_SERVER['argv'][0]; } else { $this->filename = \Phar::running(false); } $this // ->setAliases(array('selfupdate')) ->setDescription( sprintf( 'Upgrade %s to the latest version', $this->filename ) ); parent::configure(); } /** * @return string */ protected function getCurrentVersion() { return Codecept::VERSION; } /** * {@inheritdoc} */ public function execute(InputInterface $input, OutputInterface $output) { $version = $this->getCurrentVersion(); $output->writeln( sprintf( '%s version %s', self::NAME, $version ) ); $output->writeln("\nChecking for a new version...\n"); try { $latestVersion = $this->getLatestStableVersion(); if ($this->isOutOfDate($version, $latestVersion)) { $output->writeln( sprintf( 'A newer version is available: %s', $latestVersion ) ); if (!$input->getOption('no-interaction')) { $dialog = $this->getHelperSet()->get('question'); $question = new ConfirmationQuestion("\nDo you want to update? ", false); if (!$dialog->ask($input, $output, $question)) { $output->writeln("\nBye-bye!\n"); return; } } $output->writeln("\nUpdating..."); $this->retrievePharFile($latestVersion, $output); } else { $output->writeln('You are already using the latest version.'); } } catch (\Exception $e) { $output->writeln( sprintf( "\n%s\n", $e->getMessage() ) ); } } /** * Checks whether the provided version is current. * * @param string $version The version number to check. * @param string $latestVersion Latest stable version * @return boolean Returns True if a new version is available. */ private function isOutOfDate($version, $latestVersion) { return -1 != version_compare($version, $latestVersion, '>='); } /** * @return string */ private function getLatestStableVersion() { $stableVersions = $this->filterStableVersions( $this->getGithubTags(self::GITHUB_REPO) ); return array_reduce( $stableVersions, function ($a, $b) { return version_compare($a, $b, '>') ? $a : $b; } ); } /** * @param array $tags * @return array */ private function filterStableVersions($tags) { return array_filter($tags, function ($tag) { return preg_match('/^[0-9]+\.[0-9]+\.[0-9]+$/', $tag); }); } /** * Returns an array of tags from a github repo. * * @param string $repo The repository name to check upon. * @return array */ protected function getGithubTags($repo) { $jsonTags = $this->retrieveContentFromUrl( 'https://api.github.com/repos/' . $repo . '/tags' ); return array_map( function ($tag) { return $tag['name']; }, json_decode($jsonTags, true) ); } /** * Retrieves the body-content from the provided URL. * * @param string $url * @return string * @throws \Exception if status code is above 300 */ private function retrieveContentFromUrl($url) { $ctx = $this->prepareContext($url); $body = file_get_contents($url, 0, $ctx); if (isset($http_response_header)) { $code = substr($http_response_header[0], 9, 3); if (floor($code / 100) > 3) { throw new \Exception($http_response_header[0]); } } else { throw new \Exception('Request failed.'); } return $body; } /** * Add proxy support to context if environment variable was set up * * @param array $opt context options * @param string $url */ private function prepareProxy(&$opt, $url) { $scheme = parse_url($url)['scheme']; if ($scheme === 'http' && (!empty($_SERVER['HTTP_PROXY']) || !empty($_SERVER['http_proxy']))) { $proxy = !empty($_SERVER['http_proxy']) ? $_SERVER['http_proxy'] : $_SERVER['HTTP_PROXY']; } if ($scheme === 'https' && (!empty($_SERVER['HTTPS_PROXY']) || !empty($_SERVER['https_proxy']))) { $proxy = !empty($_SERVER['https_proxy']) ? $_SERVER['https_proxy'] : $_SERVER['HTTPS_PROXY']; } if (!empty($proxy)) { $proxy = str_replace(['http://', 'https://'], ['tcp://', 'ssl://'], $proxy); $opt['http']['proxy'] = $proxy; } } /** * Preparing context for request * @param $url * * @return resource */ private function prepareContext($url) { $opts = [ 'http' => [ 'follow_location' => 1, 'max_redirects' => 20, 'timeout' => 10, 'user_agent' => self::NAME ] ]; $this->prepareProxy($opts, $url); return stream_context_create($opts); } /** * Retrieves the latest phar file. * * @param string $version * @param OutputInterface $output * @throws \Exception */ protected function retrievePharFile($version, OutputInterface $output) { $temp = basename($this->filename, '.phar') . '-temp.phar'; try { $sourceUrl = $this->getPharUrl($version); if (@copy($sourceUrl, $temp)) { chmod($temp, 0777 & ~umask()); // test the phar validity $phar = new \Phar($temp); // free the variable to unlock the file unset($phar); rename($temp, $this->filename); } else { throw new \Exception('Request failed.'); } } catch (\Exception $e) { if (!$e instanceof \UnexpectedValueException && !$e instanceof \PharException ) { throw $e; } unlink($temp); $output->writeln( sprintf( "\nSomething went wrong (%s).\nPlease re-run this again.\n", $e->getMessage() ) ); } $output->writeln( sprintf( "\n%s has been updated.\n", $this->filename ) ); } /** * Returns Phar file URL for specified version * * @param string $version * @return string */ protected function getPharUrl($version) { $sourceUrl = self::PHAR_URL; if (version_compare(PHP_VERSION, '7.0.0', '<')) { $sourceUrl = self::PHAR_URL_PHP54; } return sprintf($sourceUrl, $version); } } getGlobalConfig()); } protected function getGlobalConfig($conf = null) { return Configuration::config($conf); } protected function getSuites($conf = null) { return Configuration::suites(); } protected function overrideConfig($configOptions) { $updatedConfig = []; foreach ($configOptions as $option) { $keys = explode(': ', $option); if (count($keys) < 2) { throw new \InvalidArgumentException('--config-option should have config passed as "key:value"'); } $value = array_pop($keys); $yaml = ''; for ($ind = 0; count($keys); $ind += 2) { $yaml .= "\n" . str_repeat(' ', $ind) . array_shift($keys) . ': '; } $yaml .= $value; try { $config = Yaml::parse($yaml); } catch (ParseException $e) { throw new \Codeception\Exception\ParseException("Overridden config can't be parsed: \n$yaml\n" . $e->getParsedLine()); } $updatedConfig = array_merge_recursive($updatedConfig, $config); } return Configuration::append($updatedConfig); } protected function enableExtensions($extensions) { $config = ['extensions' => ['enabled' => []]]; foreach ($extensions as $name) { if (!class_exists($name)) { $className = 'Codeception\\Extension\\' . ucfirst($name); if (!class_exists($className)) { throw new InvalidOptionException("Extension $name can't be loaded (tried by $name and $className)"); } $config['extensions']['enabled'][] = $className; continue; } $config['extensions']['enabled'][] = $name; } return Configuration::append($config); } } getFormatter()->setStyle('notice', new OutputFormatterStyle('white', 'green', ['bold'])); $output->getFormatter()->setStyle('bold', new OutputFormatterStyle(null, null, ['bold'])); $output->getFormatter()->setStyle('warning', new OutputFormatterStyle(null, 'yellow', ['bold'])); $output->getFormatter()->setStyle('debug', new OutputFormatterStyle('cyan')); } } 'Tester', 'namespace' => '', 'include' => [], 'paths' => [], 'extends' => null, 'suites' => [], 'modules' => [], 'extensions' => [ 'enabled' => [], 'config' => [], 'commands' => [], ], 'reporters' => [ 'xml' => 'Codeception\PHPUnit\Log\JUnit', 'html' => 'Codeception\PHPUnit\ResultPrinter\HTML', 'report' => 'Codeception\PHPUnit\ResultPrinter\Report', 'tap' => 'PHPUnit\Util\Log\TAP', 'json' => 'PHPUnit\Util\Log\JSON', 'phpunit-xml' => 'Codeception\PHPUnit\Log\PhpUnit', ], 'groups' => [], 'settings' => [ 'colors' => true, 'bootstrap' => false, 'strict_xml' => false, 'lint' => true, 'backup_globals' => true, 'log_incomplete_skipped' => false, 'report_useless_tests' => false, 'disallow_test_output' => false, 'be_strict_about_changes_to_global_state' => false, 'shuffle' => false, ], 'coverage' => [], 'params' => [], 'gherkin' => [] ]; public static $defaultSuiteSettings = [ 'actor' => null, 'class_name' => null, // Codeception <2.3 compatibility 'modules' => [ 'enabled' => [], 'config' => [], 'depends' => [] ], 'step_decorators' => 'Codeception\Step\ConditionalAssertion', 'path' => null, 'extends' => null, 'namespace' => null, 'groups' => [], 'formats' => [], 'shuffle' => false, 'extensions' => [ // suite extensions 'enabled' => [], 'config' => [], ], 'error_level' => 'E_ALL & ~E_STRICT & ~E_DEPRECATED', ]; protected static $params; /** * Loads global config file which is `codeception.yml` by default. * When config is already loaded - returns it. * * @param null $configFile * @return array * @throws Exception\ConfigurationException */ public static function config($configFile = null) { if (!$configFile && self::$config) { return self::$config; } if (self::$config && self::$lock) { return self::$config; } if ($configFile === null) { $configFile = getcwd() . DIRECTORY_SEPARATOR . 'codeception.yml'; } if (is_dir($configFile)) { $configFile = $configFile . DIRECTORY_SEPARATOR . 'codeception.yml'; } $dir = realpath(dirname($configFile)); self::$dir = $dir; $configDistFile = $dir . DIRECTORY_SEPARATOR . 'codeception.dist.yml'; if (!(file_exists($configDistFile) || file_exists($configFile))) { throw new ConfigurationException("Configuration file could not be found.\nRun `bootstrap` to initialize Codeception.", 404); } // Preload config to retrieve params such that they are applied to codeception config file below $tempConfig = self::$defaultConfig; $distConfigContents = ""; if (file_exists($configDistFile)) { $distConfigContents = file_get_contents($configDistFile); $tempConfig = self::mergeConfigs($tempConfig, self::getConfFromContents($distConfigContents, $configDistFile)); } $configContents = ""; if (file_exists($configFile)) { $configContents = file_get_contents($configFile); $tempConfig = self::mergeConfigs($tempConfig, self::getConfFromContents($configContents, $configFile)); } self::prepareParams($tempConfig); // load config using params $config = self::mergeConfigs(self::$defaultConfig, self::getConfFromContents($distConfigContents, $configDistFile)); $config = self::mergeConfigs($config, self::getConfFromContents($configContents, $configFile)); if ($config == self::$defaultConfig) { throw new ConfigurationException("Configuration file is invalid"); } // we check for the "extends" key in the yml file if (isset($config['extends'])) { // and now we search for the file $presetFilePath = codecept_absolute_path($config['extends']); if (file_exists($presetFilePath)) { // and merge it with our configuration file $config = self::mergeConfigs(self::getConfFromFile($presetFilePath), $config); } } self::$config = $config; // compatibility with suites created by Codeception < 2.3.0 if (!isset($config['paths']['output']) and isset($config['paths']['log'])) { $config['paths']['output'] = $config['paths']['log']; } if (isset(self::$config['actor'])) { self::$config['actor_suffix'] = self::$config['actor']; // old compatibility } if (!isset($config['paths']['support']) and isset($config['paths']['helpers'])) { $config['paths']['support'] = $config['paths']['helpers']; } if (!isset($config['paths']['output'])) { throw new ConfigurationException('Output path is not defined by key "paths: output"'); } self::$outputDir = $config['paths']['output']; // fill up includes with wildcard expansions $config['include'] = self::expandWildcardedIncludes($config['include']); // config without tests, for inclusion of other configs if (count($config['include'])) { self::$config = $config; if (!isset($config['paths']['tests'])) { return $config; } } if (!isset($config['paths']['tests'])) { throw new ConfigurationException( 'Tests directory is not defined in Codeception config by key "paths: tests:"' ); } if (!isset($config['paths']['data'])) { throw new ConfigurationException('Data path is not defined Codeception config by key "paths: data"'); } if (!isset($config['paths']['support'])) { throw new ConfigurationException('Helpers path is not defined by key "paths: support"'); } self::$dataDir = $config['paths']['data']; self::$supportDir = $config['paths']['support']; self::$testsDir = $config['paths']['tests']; if (isset($config['paths']['envs'])) { self::$envsDir = $config['paths']['envs']; } Autoload::addNamespace(self::$config['namespace'], self::supportDir()); self::loadBootstrap($config['settings']['bootstrap']); self::loadSuites(); return $config; } protected static function loadBootstrap($bootstrap) { if (!$bootstrap) { return; } $bootstrap = self::$dir . DIRECTORY_SEPARATOR . self::$testsDir . DIRECTORY_SEPARATOR . $bootstrap; if (file_exists($bootstrap)) { include_once $bootstrap; } } protected static function loadSuites() { $suites = Finder::create() ->files() ->name('*.{suite,suite.dist}.yml') ->in(self::$dir . DIRECTORY_SEPARATOR . self::$testsDir) ->depth('< 1') ->sortByName(); self::$suites = []; foreach (array_keys(self::$config['suites']) as $suite) { self::$suites[$suite] = $suite; } /** @var SplFileInfo $suite */ foreach ($suites as $suite) { preg_match('~(.*?)(\.suite|\.suite\.dist)\.yml~', $suite->getFilename(), $matches); self::$suites[$matches[1]] = $matches[1]; } } /** * Returns suite configuration. Requires suite name and global config used (Configuration::config) * * @param string $suite * @param array $config * @return array * @throws \Exception */ public static function suiteSettings($suite, $config) { // cut namespace name from suite name if ($suite != $config['namespace'] && substr($suite, 0, strlen($config['namespace'])) == $config['namespace']) { $suite = substr($suite, strlen($config['namespace'])); } if (!in_array($suite, self::$suites)) { throw new ConfigurationException("Suite $suite was not loaded"); } // load global config $globalConf = $config['settings']; foreach (['modules', 'coverage', 'namespace', 'groups', 'env', 'gherkin', 'extensions'] as $key) { if (isset($config[$key])) { $globalConf[$key] = $config[$key]; } } $settings = self::mergeConfigs(self::$defaultSuiteSettings, $globalConf); // load suite config $settings = self::loadSuiteConfig($suite, $config['paths']['tests'], $settings); // load from environment configs if (isset($config['paths']['envs'])) { $envConf = self::loadEnvConfigs(self::$dir . DIRECTORY_SEPARATOR . $config['paths']['envs']); $settings = self::mergeConfigs($settings, $envConf); } if (!$settings['actor']) { // Codeception 2.2 compatibility $settings['actor'] = $settings['class_name']; } if (!$settings['path']) { // take a suite path from its name $settings['path'] = $suite; } $config['paths']['tests'] = str_replace('/', DIRECTORY_SEPARATOR, $config['paths']['tests']); $settings['path'] = self::$dir . DIRECTORY_SEPARATOR . $config['paths']['tests'] . DIRECTORY_SEPARATOR . $settings['path'] . DIRECTORY_SEPARATOR; return $settings; } /** * Loads environments configuration from set directory * * @param string $path path to the directory * @return array */ protected static function loadEnvConfigs($path) { if (isset(self::$envConfig[$path])) { return self::$envConfig[$path]; } if (!is_dir($path)) { self::$envConfig[$path] = []; return self::$envConfig[$path]; } $envFiles = Finder::create() ->files() ->name('*.yml') ->in($path) ->depth('< 2'); $envConfig = []; /** @var SplFileInfo $envFile */ foreach ($envFiles as $envFile) { $env = str_replace(['.dist.yml', '.yml'], '', $envFile->getFilename()); $envConfig[$env] = []; $envPath = $path; if ($envFile->getRelativePath()) { $envPath .= DIRECTORY_SEPARATOR . $envFile->getRelativePath(); } foreach (['.dist.yml', '.yml'] as $suffix) { $envConf = self::getConfFromFile($envPath . DIRECTORY_SEPARATOR . $env . $suffix, null); if ($envConf === null) { continue; } $envConfig[$env] = self::mergeConfigs($envConfig[$env], $envConf); } } self::$envConfig[$path] = ['env' => $envConfig]; return self::$envConfig[$path]; } /** * Loads configuration from Yaml data * * @param string $contents Yaml config file contents * @param string $filename which is supposed to be loaded * @return array * @throws ConfigurationException */ protected static function getConfFromContents($contents, $filename = '(.yml)') { if (self::$params) { $template = new Template($contents, '%', '%'); $template->setVars(self::$params); $contents = $template->produce(); } try { return Yaml::parse($contents); } catch (ParseException $exception) { throw new ConfigurationException( sprintf( "Error loading Yaml config from `%s`\n \n%s\nRead more about Yaml format https://goo.gl/9UPuEC", $filename, $exception->getMessage() ) ); } } /** * Loads configuration from Yaml file or returns given value if the file doesn't exist * * @param string $filename filename * @param mixed $nonExistentValue value used if filename is not found * @return array * @throws ConfigurationException */ protected static function getConfFromFile($filename, $nonExistentValue = []) { if (file_exists($filename)) { $yaml = file_get_contents($filename); return self::getConfFromContents($yaml, $filename); } return $nonExistentValue; } /** * Returns all possible suite configurations according environment rules. * Suite configurations will contain `current_environment` key which specifies what environment used. * * @param $suite * @return array * @throws ConfigurationException */ public static function suiteEnvironments($suite) { $settings = self::suiteSettings($suite, self::config()); if (!isset($settings['env']) || !is_array($settings['env'])) { return []; } $environments = []; foreach ($settings['env'] as $env => $envConfig) { $environments[$env] = $envConfig ? self::mergeConfigs($settings, $envConfig) : $settings; $environments[$env]['current_environment'] = $env; } return $environments; } public static function suites() { return self::$suites; } /** * Return list of enabled modules according suite config. * * @param array $settings suite settings * @return array */ public static function modules($settings) { return array_filter( array_map( function ($m) { return is_array($m) ? key($m) : $m; }, $settings['modules']['enabled'], array_keys($settings['modules']['enabled']) ), function ($m) use ($settings) { if (!isset($settings['modules']['disabled'])) { return true; } return !in_array($m, $settings['modules']['disabled']); } ); } public static function isExtensionEnabled($extensionName) { return isset(self::$config['extensions'], self::$config['extensions']['enabled']) && in_array($extensionName, self::$config['extensions']['enabled']); } /** * Returns current path to `_data` dir. * Use it to store database fixtures, sql dumps, or other files required by your tests. * * @return string */ public static function dataDir() { return self::$dir . DIRECTORY_SEPARATOR . self::$dataDir . DIRECTORY_SEPARATOR; } /** * Return current path to `_helpers` dir. * Helpers are custom modules. * * @return string */ public static function supportDir() { return self::$dir . DIRECTORY_SEPARATOR . self::$supportDir . DIRECTORY_SEPARATOR; } /** * Returns actual path to current `_output` dir. * Use it in Helpers or Groups to save result or temporary files. * * @return string * @throws Exception\ConfigurationException */ public static function outputDir() { if (!self::$outputDir) { throw new ConfigurationException("Path for output not specified. Please, set output path in global config"); } $dir = self::$outputDir . DIRECTORY_SEPARATOR; if (strcmp(self::$outputDir[0], "/") !== 0) { $dir = self::$dir . DIRECTORY_SEPARATOR . $dir; } if (!file_exists($dir)) { @mkdir($dir, 0777, true); } if (!is_writable($dir)) { @chmod($dir, 0777); } if (!is_writable($dir)) { throw new ConfigurationException( "Path for output is not writable. Please, set appropriate access mode for output path: {$dir}" ); } return $dir; } /** * Compatibility alias to `Configuration::logDir()` * @return string * @throws ConfigurationException */ public static function logDir() { return self::outputDir(); } /** * Returns path to the root of your project. * Basically returns path to current `codeception.yml` loaded. * Use this method instead of `__DIR__`, `getcwd()` or anything else. * @return string */ public static function projectDir() { return self::$dir . DIRECTORY_SEPARATOR; } /** * Returns path to tests directory * * @return string */ public static function testsDir() { return self::$dir . DIRECTORY_SEPARATOR . self::$testsDir . DIRECTORY_SEPARATOR; } /** * Return current path to `_envs` dir. * Use it to store environment specific configuration. * * @return string */ public static function envsDir() { if (!self::$envsDir) { return null; } return self::$dir . DIRECTORY_SEPARATOR . self::$envsDir . DIRECTORY_SEPARATOR; } /** * Is this a meta-configuration file that just points to other `codeception.yml`? * If so, it may have no tests by itself. * * @return bool */ public static function isEmpty() { return !(bool)self::$testsDir; } /** * Adds parameters to config * * @param array $config * @return array */ public static function append(array $config = []) { self::$config = self::mergeConfigs(self::$config, $config); if (isset(self::$config['paths']['output'])) { self::$outputDir = self::$config['paths']['output']; } if (isset(self::$config['paths']['data'])) { self::$dataDir = self::$config['paths']['data']; } if (isset(self::$config['paths']['support'])) { self::$supportDir = self::$config['paths']['support']; } if (isset(self::$config['paths']['tests'])) { self::$testsDir = self::$config['paths']['tests']; } return self::$config; } public static function mergeConfigs($a1, $a2) { if (!is_array($a1)) { return $a2; } if (!is_array($a2)) { return $a1; } $res = []; // for sequential arrays if (isset($a1[0], $a2[0])) { return array_merge_recursive($a2, $a1); } // for associative arrays foreach ($a2 as $k2 => $v2) { if (!isset($a1[$k2])) { // if no such key $res[$k2] = $v2; unset($a1[$k2]); continue; } $res[$k2] = self::mergeConfigs($a1[$k2], $v2); unset($a1[$k2]); } foreach ($a1 as $k1 => $v1) { // only single elements here left $res[$k1] = $v1; } return $res; } /** * Loads config from *.dist.suite.yml and *.suite.yml * * @param $suite * @param $path * @param $settings * @return array * @throws ConfigurationException */ protected static function loadSuiteConfig($suite, $path, $settings) { if (isset(self::$config['suites'][$suite])) { // bundled config return self::mergeConfigs($settings, self::$config['suites'][$suite]); } $suiteDir = self::$dir . DIRECTORY_SEPARATOR . $path; $suiteDistConf = self::getConfFromFile($suiteDir . DIRECTORY_SEPARATOR . "$suite.suite.dist.yml"); $suiteConf = self::getConfFromFile($suiteDir . DIRECTORY_SEPARATOR . "$suite.suite.yml"); // now we check the suite config file, if a extends key is defined if (isset($suiteConf['extends'])) { $presetFilePath = codecept_is_path_absolute($suiteConf['extends']) ? $suiteConf['extends'] // If path is absolute – use it : realpath($suiteDir . DIRECTORY_SEPARATOR . $suiteConf['extends']); // Otherwise try to locate it in the suite dir if (file_exists($presetFilePath)) { $settings = self::mergeConfigs(self::getConfFromFile($presetFilePath), $settings); } } $settings = self::mergeConfigs($settings, $suiteDistConf); $settings = self::mergeConfigs($settings, $suiteConf); return $settings; } /** * Replaces wildcarded items in include array with real paths. * * @param $includes * @return array * @throws ConfigurationException */ protected static function expandWildcardedIncludes(array $includes) { if (empty($includes)) { return $includes; } $expandedIncludes = []; foreach ($includes as $include) { $expandedIncludes = array_merge($expandedIncludes, self::expandWildcardsFor($include)); } return $expandedIncludes; } /** * Finds config files in given wildcarded include path. * Returns the expanded paths or the original if not a wildcard. * * @param $include * @return array * @throws ConfigurationException */ protected static function expandWildcardsFor($include) { if (1 !== preg_match('/[\?\.\*]/', $include)) { return [$include,]; } try { $configFiles = Finder::create()->files() ->name('/codeception(\.dist\.yml|\.yml)/') ->in(self::$dir . DIRECTORY_SEPARATOR . $include); } catch (\InvalidArgumentException $e) { throw new ConfigurationException( "Configuration file(s) could not be found in \"$include\"." ); } $paths = []; foreach ($configFiles as $file) { $paths[] = codecept_relative_path($file->getPath()); } return $paths; } private static function prepareParams($settings) { self::$params = []; $paramsLoader = new ParamsLoader(); foreach ($settings['params'] as $paramStorage) { static::$params = array_merge(self::$params, $paramsLoader->load($paramStorage)); } } } phpCodeCoverage = $phpCoverage ? $phpCoverage : new \SebastianBergmann\CodeCoverage\CodeCoverage; $this->filter = $this->phpCodeCoverage->filter(); } /** * @param \SebastianBergmann\CodeCoverage\CodeCoverage $phpCoverage * @return Filter */ public static function setup(\SebastianBergmann\CodeCoverage\CodeCoverage $phpCoverage) { self::$c3 = new self($phpCoverage); return self::$c3; } /** * @return null|\SebastianBergmann\CodeCoverage\CodeCoverage */ public function getPhpCodeCoverage() { return $this->phpCodeCoverage; } /** * @param $config * @return Filter */ public function whiteList($config) { $filter = $this->filter; if (!isset($config['coverage'])) { return $this; } $coverage = $config['coverage']; if (!isset($coverage['whitelist'])) { $coverage['whitelist'] = []; if (isset($coverage['include'])) { $coverage['whitelist']['include'] = $coverage['include']; } if (isset($coverage['exclude'])) { $coverage['whitelist']['exclude'] = $coverage['exclude']; } } if (isset($coverage['whitelist']['include'])) { if (!is_array($coverage['whitelist']['include'])) { throw new ConfigurationException('Error parsing yaml. Config `whitelist: include:` should be an array'); } foreach ($coverage['whitelist']['include'] as $fileOrDir) { $finder = strpos($fileOrDir, '*') === false ? [Configuration::projectDir() . DIRECTORY_SEPARATOR . $fileOrDir] : $this->matchWildcardPattern($fileOrDir); foreach ($finder as $file) { $filter->addFileToWhitelist($file); } } } if (isset($coverage['whitelist']['exclude'])) { if (!is_array($coverage['whitelist']['exclude'])) { throw new ConfigurationException('Error parsing yaml. Config `whitelist: exclude:` should be an array'); } foreach ($coverage['whitelist']['exclude'] as $fileOrDir) { $finder = strpos($fileOrDir, '*') === false ? [Configuration::projectDir() . DIRECTORY_SEPARATOR . $fileOrDir] : $this->matchWildcardPattern($fileOrDir); foreach ($finder as $file) { $filter->removeFileFromWhitelist($file); } } } return $this; } /** * @param $config * @return Filter */ public function blackList($config) { $filter = $this->filter; if (!isset($config['coverage'])) { return $this; } $coverage = $config['coverage']; if (isset($coverage['blacklist'])) { if (!method_exists($filter, 'addFileToBlacklist')) { throw new ModuleException($this, 'The blacklist functionality has been removed from PHPUnit 5,' . ' please remove blacklist section from configuration.'); } if (isset($coverage['blacklist']['include'])) { foreach ($coverage['blacklist']['include'] as $fileOrDir) { $finder = strpos($fileOrDir, '*') === false ? [Configuration::projectDir() . DIRECTORY_SEPARATOR . $fileOrDir] : $this->matchWildcardPattern($fileOrDir); foreach ($finder as $file) { $filter->addFileToBlacklist($file); } } } if (isset($coverage['blacklist']['exclude'])) { foreach ($coverage['blacklist']['exclude'] as $fileOrDir) { $finder = strpos($fileOrDir, '*') === false ? [Configuration::projectDir() . DIRECTORY_SEPARATOR . $fileOrDir] : $this->matchWildcardPattern($fileOrDir); foreach ($finder as $file) { $filter->removeFileFromBlacklist($file); } } } } return $this; } protected function matchWildcardPattern($pattern) { $finder = Finder::create(); $fileOrDir = str_replace('\\', '/', $pattern); $parts = explode('/', $fileOrDir); $file = array_pop($parts); $finder->name($file); if (count($parts)) { $last_path = array_pop($parts); if ($last_path === '*') { $finder->in(Configuration::projectDir() . implode('/', $parts)); } else { $finder->in(Configuration::projectDir() . implode('/', $parts) . '/' . $last_path); } } $finder->ignoreVCS(true)->files(); return $finder; } /** * @return \SebastianBergmann\CodeCoverage\Filter */ public function getFilter() { return $this->filter; } } 'beforeSuite', Events::SUITE_AFTER => 'afterSuite', ]; /** * @var Remote */ protected $module; protected function isEnabled() { return $this->module === null and $this->settings['enabled']; } public function beforeSuite(SuiteEvent $e) { $this->applySettings($e->getSettings()); $this->module = $this->getServerConnectionModule($e->getSuite()->getModules()); if (!$this->isEnabled()) { return; } $this->applyFilter($e->getResult()); } public function afterSuite(SuiteEvent $e) { if (!$this->isEnabled()) { return; } $this->mergeToPrint($e->getResult()->getCodeCoverage()); } } [ 'method' => "GET", 'header' => '' ] ]; /** * @var \Codeception\Lib\Interfaces\Web */ protected $module; public static $events = [ Events::SUITE_BEFORE => 'beforeSuite', Events::TEST_BEFORE => 'beforeTest', Events::STEP_AFTER => 'afterStep', Events::SUITE_AFTER => 'afterSuite', ]; protected function isEnabled() { return $this->module && !$this->settings['remote'] && $this->settings['enabled']; } public function beforeSuite(SuiteEvent $e) { $this->module = $this->getServerConnectionModule($e->getSuite()->getModules()); $this->applySettings($e->getSettings()); if (!$this->isEnabled()) { return; } $this->suiteName = $e->getSuite()->getBaseName(); if ($this->settings['remote_config']) { $this->addC3AccessHeader(self::COVERAGE_HEADER_CONFIG, $this->settings['remote_config']); } $knock = $this->c3Request('clear'); if ($knock === false) { throw new RemoteException( ' CodeCoverage Error. Check the file "c3.php" is included in your application. We tried to access "/c3/report/clear" but this URI was not accessible. You can review actual error messages in c3tmp dir. ' ); } } public function beforeTest(TestEvent $e) { if (!$this->isEnabled()) { return; } $this->startCoverageCollection($e->getTest()->getName()); } public function afterStep(StepEvent $e) { if (!$this->isEnabled()) { return; } $this->fetchErrors(); } public function afterSuite(SuiteEvent $e) { if (!$this->isEnabled()) { return; } $coverageFile = Configuration::outputDir() . 'c3tmp/codecoverage.serialized'; $retries = 5; while (!file_exists($coverageFile) && --$retries >= 0) { usleep(0.5 * 1000000); // 0.5 sec } if (!file_exists($coverageFile)) { if (file_exists(Configuration::outputDir() . 'c3tmp/error.txt')) { throw new \RuntimeException(file_get_contents(Configuration::outputDir() . 'c3tmp/error.txt')); } return; } $contents = file_get_contents($coverageFile); $coverage = @unserialize($contents); if ($coverage === false) { return; } $this->preProcessCoverage($coverage) ->mergeToPrint($coverage); } /** * Allows Translating Remote Paths To Local (IE: When Using Docker) * * @param \SebastianBergmann\CodeCoverage\CodeCoverage $coverage * @return $this */ protected function preProcessCoverage($coverage) { //Only Process If Work Directory Set if ($this->settings['work_dir'] === null) { return $this; } $workDir = rtrim($this->settings['work_dir'], '/\\') . DIRECTORY_SEPARATOR; $projectDir = Configuration::projectDir(); $data = $coverage->getData(true); //We only want covered files, not all whitelisted ones. codecept_debug("Replacing all instances of {$workDir} with {$projectDir}"); foreach ($data as $path => $datum) { unset($data[$path]); $path = str_replace($workDir, $projectDir, $path); $data[$path] = $datum; } $coverage->setData($data); return $this; } protected function c3Request($action) { $this->addC3AccessHeader(self::COVERAGE_HEADER, 'remote-access'); $context = stream_context_create($this->c3Access); $c3Url = $this->settings['c3_url'] ? $this->settings['c3_url'] : $this->module->_getUrl(); $contents = file_get_contents($c3Url . '/c3/report/' . $action, false, $context); $okHeaders = array_filter( $http_response_header, function ($h) { return preg_match('~^HTTP(.*?)\s200~', $h); } ); if (empty($okHeaders)) { throw new RemoteException("Request was not successful. See response header: " . $http_response_header[0]); } if ($contents === false) { $this->getRemoteError($http_response_header); } return $contents; } protected function startCoverageCollection($testName) { $value = [ 'CodeCoverage' => $testName, 'CodeCoverage_Suite' => $this->suiteName, 'CodeCoverage_Config' => $this->settings['remote_config'] ]; $value = json_encode($value); if ($this->module instanceof \Codeception\Module\WebDriver) { $this->module->amOnPage('/'); } $cookieDomain = isset($this->settings['cookie_domain']) ? $this->settings['cookie_domain'] : null; if (!$cookieDomain) { $c3Url = parse_url($this->settings['c3_url'] ? $this->settings['c3_url'] : $this->module->_getUrl()); // we need to separate coverage cookies by host; we can't separate cookies by port. $cookieDomain = isset($c3Url['host']) ? $c3Url['host'] : 'localhost'; } $this->module->setCookie(self::COVERAGE_COOKIE, $value, ['domain' => $cookieDomain]); // putting in configuration ensures the cookie is used for all sessions of a MultiSession test $cookies = $this->module->_getConfig('cookies'); if (!$cookies || !is_array($cookies)) { $cookies = []; } $found = false; foreach ($cookies as &$cookie) { if (!is_array($cookie) || !array_key_exists('Name', $cookie) || !array_key_exists('Value', $cookie)) { // \Codeception\Lib\InnerBrowser will complain about this continue; } if ($cookie['Name'] === self::COVERAGE_COOKIE) { $found = true; $cookie['Value'] = $value; break; } } unset($cookie); if (!$found) { $cookies[] = [ 'Name' => self::COVERAGE_COOKIE, 'Value' => $value ]; } $this->module->_setConfig(['cookies' => $cookies]); } protected function fetchErrors() { try { $error = $this->module->grabCookie(self::COVERAGE_COOKIE_ERROR); } catch (ModuleException $e) { // when a new session is started we can't get cookies because there is no // current page, but there can be no code coverage error either $error = null; } if (!empty($error)) { $this->module->resetCookie(self::COVERAGE_COOKIE_ERROR); throw new RemoteException($error); } } protected function getRemoteError($headers) { foreach ($headers as $header) { if (strpos($header, self::COVERAGE_HEADER_ERROR) === 0) { throw new RemoteException($header); } } } protected function addC3AccessHeader($header, $value) { $headerString = "$header: $value\r\n"; if (strpos($this->c3Access['http']['header'], $headerString) === false) { $this->c3Access['http']['header'] .= $headerString; } } protected function applySettings($settings) { parent::applySettings($settings); if (isset($settings['coverage']['remote_context_options'])) { $this->c3Access = array_replace_recursive($this->c3Access, $settings['coverage']['remote_context_options']); } } } 'printResult' ]; protected $settings = [ 'enabled' => true, 'low_limit' => '35', 'high_limit' => '70', 'show_uncovered' => false, 'show_only_summary' => false ]; public static $coverage; protected $options; protected $logDir; protected $destination = []; public function __construct($options) { $this->options = $options; $this->logDir = Configuration::outputDir(); $this->settings = array_merge($this->settings, Configuration::config()['coverage']); self::$coverage = new \SebastianBergmann\CodeCoverage\CodeCoverage(); // Apply filter $filter = new Filter(self::$coverage); $filter ->whiteList(Configuration::config()) ->blackList(Configuration::config()); } protected function absolutePath($path) { if ((strpos($path, '/') === 0) || (strpos($path, ':') === 1)) { // absolute path return $path; } return $this->logDir . $path; } public function printResult(PrintResultEvent $e) { $printer = $e->getPrinter(); if (!$this->settings['enabled']) { $printer->write("\nCodeCoverage is disabled in `codeception.yml` config\n"); return; } if (!$this->options['quiet']) { $this->printConsole($printer); } $printer->write("Remote CodeCoverage reports are not printed to console\n"); $this->printPHP(); $printer->write("\n"); if ($this->options['coverage-html']) { $this->printHtml(); $printer->write("HTML report generated in {$this->options['coverage-html']}\n"); } if ($this->options['coverage-xml']) { $this->printXml(); $printer->write("XML report generated in {$this->options['coverage-xml']}\n"); } if ($this->options['coverage-text']) { $this->printText(); $printer->write("Text report generated in {$this->options['coverage-text']}\n"); } if ($this->options['coverage-crap4j']) { $this->printCrap4j(); $printer->write("Crap4j report generated in {$this->options['coverage-crap4j']}\n"); } if ($this->options['coverage-phpunit']) { $this->printPHPUnit(); $printer->write("PHPUnit report generated in {$this->options['coverage-phpunit']}\n"); } } protected function printConsole(\PHPUnit\Util\Printer $printer) { $writer = new \SebastianBergmann\CodeCoverage\Report\Text( $this->settings['low_limit'], $this->settings['high_limit'], $this->settings['show_uncovered'], $this->settings['show_only_summary'] ); $printer->write($writer->process(self::$coverage, $this->options['colors'])); } protected function printHtml() { $writer = new \SebastianBergmann\CodeCoverage\Report\Html\Facade( $this->settings['low_limit'], $this->settings['high_limit'], sprintf( ', Codeception and PHPUnit %s', \PHPUnit\Runner\Version::id() ) ); $writer->process(self::$coverage, $this->absolutePath($this->options['coverage-html'])); } protected function printXml() { $writer = new \SebastianBergmann\CodeCoverage\Report\Clover(); $writer->process(self::$coverage, $this->absolutePath($this->options['coverage-xml'])); } protected function printPHP() { $writer = new \SebastianBergmann\CodeCoverage\Report\PHP; $writer->process(self::$coverage, $this->absolutePath($this->options['coverage'])); } protected function printText() { $writer = new \SebastianBergmann\CodeCoverage\Report\Text( $this->settings['low_limit'], $this->settings['high_limit'], $this->settings['show_uncovered'], $this->settings['show_only_summary'] ); file_put_contents( $this->absolutePath($this->options['coverage-text']), $writer->process(self::$coverage, false) ); } protected function printCrap4j() { $writer = new \SebastianBergmann\CodeCoverage\Report\Crap4j; $writer->process(self::$coverage, $this->absolutePath($this->options['coverage-crap4j'])); } protected function printPHPUnit() { $writer = new \SebastianBergmann\CodeCoverage\Report\Xml\Facade(\PHPUnit\Runner\Version::id()); $writer->process(self::$coverage, $this->absolutePath($this->options['coverage-phpunit'])); } } module and $this->settings['remote'] and $this->settings['enabled']; } public function afterSuite(SuiteEvent $e) { if (!$this->isEnabled()) { return; } $suite = strtr($e->getSuite()->getName(), ['\\' => '.']); if ($this->options['coverage-xml']) { $this->retrieveAndPrintXml($suite); } if ($this->options['coverage-html']) { $this->retrieveAndPrintHtml($suite); } if ($this->options['coverage-crap4j']) { $this->retrieveAndPrintCrap4j($suite); } if ($this->options['coverage-phpunit']) { $this->retrieveAndPrintPHPUnit($suite); } } protected function retrieveAndPrintHtml($suite) { $tempFile = tempnam(sys_get_temp_dir(), 'C3') . '.tar'; file_put_contents($tempFile, $this->c3Request('html')); $destDir = Configuration::outputDir() . $suite . '.remote.coverage'; if (is_dir($destDir)) { FileSystem::doEmptyDir($destDir); } else { mkdir($destDir, 0777, true); } $phar = new \PharData($tempFile); $phar->extractTo($destDir); unlink($tempFile); } protected function retrieveAndPrintXml($suite) { $destFile = Configuration::outputDir() . $suite . '.remote.coverage.xml'; file_put_contents($destFile, $this->c3Request('clover')); } protected function retrieveAndPrintCrap4j($suite) { $destFile = Configuration::outputDir() . $suite . '.remote.crap4j.xml'; file_put_contents($destFile, $this->c3Request('crap4j')); } protected function retrieveAndPrintPHPUnit($suite) { $tempFile = tempnam(sys_get_temp_dir(), 'C3') . '.tar'; file_put_contents($tempFile, $this->c3Request('phpunit')); $destDir = Configuration::outputDir() . $suite . '.remote.coverage-phpunit'; if (is_dir($destDir)) { FileSystem::doEmptyDir($destDir); } else { mkdir($destDir, 0777, true); } $phar = new \PharData($tempFile); $phar->extractTo($destDir); unlink($tempFile); } } false, 'remote' => false, 'local' => false, 'xdebug_session' => 'codeception', 'remote_config' => null, 'show_uncovered' => false, 'c3_url' => null, 'work_dir' => null, 'cookie_domain' => null, ]; protected $settings = []; protected $filters = []; protected $modules = []; protected $coverage; protected $logDir; protected $options; public static $events = []; abstract protected function isEnabled(); public function __construct($options = []) { $this->options = $options; $this->logDir = Configuration::outputDir(); } protected function applySettings($settings) { try { $this->coverage = new \SebastianBergmann\CodeCoverage\CodeCoverage(); } catch (CodeCoverageException $e) { throw new \Exception( 'XDebug is required to collect CodeCoverage. Please install xdebug extension and enable it in php.ini' ); } $this->filters = $settings; $this->settings = $this->defaultSettings; $keys = array_keys($this->defaultSettings); foreach ($keys as $key) { if (isset($settings['coverage'][$key])) { $this->settings[$key] = $settings['coverage'][$key]; } } $this->coverage->setProcessUncoveredFilesFromWhitelist($this->settings['show_uncovered']); } /** * @param array $modules * @return \Codeception\Lib\Interfaces\Remote|null */ protected function getServerConnectionModule(array $modules) { foreach ($modules as $module) { if ($module instanceof Remote) { return $module; } } return null; } public function applyFilter(\PHPUnit\Framework\TestResult $result) { $driver = Stub::makeEmpty('SebastianBergmann\CodeCoverage\Driver\Driver'); $result->setCodeCoverage(new CodeCoverage($driver)); Filter::setup($this->coverage) ->whiteList($this->filters) ->blackList($this->filters); $result->setCodeCoverage($this->coverage); } protected function mergeToPrint($coverage) { Printer::$coverage->merge($coverage); } } fail = $e; $this->count = $count; } public function getCount() { return $this->count; } public function getFail() { return $this->fail; } } result = $result; $this->printer = $printer; } /** * @return \PHPUnit\Util\Printer */ public function getPrinter() { return $this->printer; } /** * @return \PHPUnit\Framework\TestResult */ public function getResult() { return $this->result; } } test = $test; $this->step = $step; } public function getStep() { return $this->step; } /** * @return TestInterface */ public function getTest() { return $this->test; } } suite = $suite; $this->result = $result; $this->settings = $settings; } /** * @return Suite */ public function getSuite() { return $this->suite; } /** * @return \PHPUnit\Framework\TestResult */ public function getResult() { return $this->result; } public function getSettings() { return $this->settings; } } test = $test; $this->time = $time; } /** * @return float */ public function getTime() { return $this->time; } /** * @return \Codeception\TestInterface */ public function getTest() { return $this->test; } } */ final class Events { /** * Private constructor. This class cannot be instantiated. */ private function __construct() { } /** * The MODULE_INIT event occurs before modules are initialized. * * The event listener method receives a {@link Codeception\Event\SuiteEvent} instance. */ const MODULE_INIT = 'module.init'; /** * The SUITE_INIT event occurs when suite is initialized. * Modules are created and initialized, but Actor class is not loaded. * * The event listener method receives a {@link Codeception\Event\SuiteEvent} instance. */ const SUITE_INIT = 'suite.init'; /** * The SUITE_BEFORE event occurs before suite is executed. * * The event listener method receives a {@link Codeception\Event\SuiteEvent} instance. */ const SUITE_BEFORE = 'suite.before'; /** * The SUITE_AFTER event occurs after suite has been executed. * * The event listener method receives a {@link Codeception\Event\SuiteEvent} instance. */ const SUITE_AFTER = 'suite.after'; /** * The event listener method receives a {@link Codeception\Event\TestEvent} instance. */ const TEST_START = 'test.start'; /** * The event listener method receives a {@link Codeception\Event\TestEvent} instance. */ const TEST_BEFORE = 'test.before'; /** * The event listener method receives a {@link Codeception\Event\StepEvent} instance. */ const STEP_BEFORE = 'step.before'; /** * The event listener method receives a {@link Codeception\Event\StepEvent} instance. */ const STEP_AFTER = 'step.after'; /** * The TEST_FAIL event occurs whenever test has failed. * * The event listener method receives a {@link Codeception\Event\FailEvent} instance. */ const TEST_FAIL = 'test.fail'; /** * The TEST_ERROR event occurs whenever test got an error while being executed. * * The event listener method receives a {@link Codeception\Event\FailEvent} instance. */ const TEST_ERROR = 'test.error'; /** * The event listener method receives a {@link Codeception\Event\TestEvent} instance. */ const TEST_PARSED = 'test.parsed'; /** * The event listener method receives a {@link Codeception\Event\FailEvent} instance. */ const TEST_INCOMPLETE = 'test.incomplete'; /** * The event listener method receives a {@link Codeception\Event\FailEvent} instance. */ const TEST_SKIPPED = 'test.skipped'; /** * The event listener method receives a {@link Codeception\Event\FailEvent} instance. */ const TEST_WARNING = 'test.warning'; /** * The event listener method receives a {@link Codeception\Event\TestEvent} instance. */ const TEST_SUCCESS = 'test.success'; /** * The event listener method receives a {@link Codeception\Event\TestEvent} instance. */ const TEST_AFTER = 'test.after'; /** * The event listener method receives a {@link Codeception\Event\TestEvent} instance. */ const TEST_END = 'test.end'; /** * The event listener method receives a {@link Codeception\Event\FailEvent} instance. */ const TEST_FAIL_PRINT = 'test.fail.print'; /** * The event listener method receives a {@link Codeception\Event\PrintResultEvent} instance. */ const RESULT_PRINT_AFTER = 'result.print.after'; } data = $data; } /** * Whether a offset exists * @link http://php.net/manual/en/arrayaccess.offsetexists.php * @param mixed $offset

* An offset to check for. *

* @return boolean true on success or false on failure. *

*

* The return value will be casted to boolean if non-boolean was returned. * @since 5.0.0 */ public function offsetExists($offset) { return array_key_exists($offset, $this->data); } /** * Offset to retrieve * @link http://php.net/manual/en/arrayaccess.offsetget.php * @param mixed $offset

* The offset to retrieve. *

* @return mixed Can return all value types. * @since 5.0.0 */ public function offsetGet($offset) { if (!$this->offsetExists($offset)) { throw new \PHPUnit\Framework\AssertionFailedError("Example $offset doesn't exist"); }; return $this->data[$offset]; } /** * Offset to set * @link http://php.net/manual/en/arrayaccess.offsetset.php * @param mixed $offset

* The offset to assign the value to. *

* @param mixed $value

* The value to set. *

* @return void * @since 5.0.0 */ public function offsetSet($offset, $value) { $this->data[$offset] = $value; } /** * Offset to unset * @link http://php.net/manual/en/arrayaccess.offsetunset.php * @param mixed $offset

* The offset to unset. *

* @return void * @since 5.0.0 */ public function offsetUnset($offset) { unset($this->data[$offset]); } /** * Count elements of an object * @link http://php.net/manual/en/countable.count.php * @return int The custom count as an integer. *

*

* The return value is cast to an integer. * @since 5.1.0 */ public function count() { return count($this->data); } /** * Retrieve an external iterator * @link http://php.net/manual/en/iteratoraggregate.getiterator.php * @return Traversable An instance of an object implementing Iterator or * Traversable * @since 5.0.0 */ public function getIterator() { return new \ArrayIterator($this->data); } } message = $extension . "\n\n" . $this->message; } } message = $module . " module is not configured!\n \n" . $this->message; } } message = "$module module conflicts with $conflicted\n\n--\n" . "This usually happens when you enable two modules with the same actions but with different backends.\n" . "For instance, you can't run PhpBrowser, WebDriver, Laravel5 modules in one suite,\n" . "as they implement similar methods but use different drivers to execute them.\n" . "You can load a part of module (like: ORM) to avoid conflict.\n" . $additional; } } module = $module; parent::__construct($message); $this->message = "$module: {$this->message}"; } } message = "[$module] module requirements not met --\n \n" . $this->message; } } message = "Remote Application Error:\n" . $this->message; } } message = "Couldn't parse test '$fileName' on line $line"; } else { $this->message = "Couldn't parse test '$fileName'"; } if ($errors) { $this->message .= "\n$errors"; } } } config = array_merge($this->config, $config); $this->options = $options; $this->output = new Output($options); $this->_initialize(); } public static function getSubscribedEvents() { if (!isset(static::$events)) { return [Events::SUITE_INIT => 'receiveModuleContainer']; } if (isset(static::$events[Events::SUITE_INIT])) { if (!is_array(static::$events[Events::SUITE_INIT])) { static::$events[Events::SUITE_INIT] = [[static::$events[Events::SUITE_INIT]]]; } static::$events[Events::SUITE_INIT][] = ['receiveModuleContainer']; } else { static::$events[Events::SUITE_INIT] = 'receiveModuleContainer'; } return static::$events; } public function receiveModuleContainer(SuiteEvent $e) { $this->modules = $e->getSuite()->getModules(); } /** * Pass config variables that should be injected into global config. * * @param array $config */ public function _reconfigure($config = []) { if (is_array($config)) { Config::append($config); } } /** * You can do all preparations here. No need to override constructor. * Also you can skip calling `_reconfigure` if you don't need to. */ public function _initialize() { $this->_reconfigure(); // hook for BC only. } protected function write($message) { if (!$this->options['silent']) { $this->output->write($message); } } protected function writeln($message) { if (!$this->options['silent']) { $this->output->writeln($message); } } public function hasModule($name) { return isset($this->modules[$name]); } public function getCurrentModuleNames() { return array_keys($this->modules); } public function getModule($name) { if (!$this->hasModule($name)) { throw new ModuleRequireException($name, "module is not enabled"); } return $this->modules[$name]; } public function getTestsDir() { return Config::testsDir(); } public function getLogDir() { return Config::outputDir(); } public function getDataDir() { return Config::dataDir(); } public function getRootDir() { return Config::projectDir(); } public function getGlobalConfig() { return Config::config(); } } '_before', Events::TEST_AFTER . '.' . static::$group => '_after', ]; } return array_merge($events, $inheritedEvents); } } input = $input; $this->addStyles($output); $this->output = $output; } /** * Change the directory where Codeception should be installed. */ public function initDir($workDir) { $this->checkInstalled($workDir); $this->sayInfo("Initializing Codeception in $workDir"); $this->createDirectoryFor($workDir); chdir($workDir); $this->workDir = $workDir; } /** * Override this class to create customized setup. * @return mixed */ abstract public function setup(); /** * ```php * ask('select the browser of your choice', 'firefox'); * * // propose firefox or chrome possible options * $this->ask('select the browser of your choice', ['firefox', 'chrome']); * * // ask true/false question * $this->ask('do you want to proceed (y/n)', true); * ``` * * @param $question * @param null $answer * @return mixed|string */ protected function ask($question, $answer = null) { $question = "? $question"; $dialog = new QuestionHelper(); if (is_array($answer)) { $question .= " (" . $answer[0] . ") "; return $dialog->ask($this->input, $this->output, new ChoiceQuestion($question, $answer, 0)); } if (is_bool($answer)) { $question .= " (y/n) "; return $dialog->ask($this->input, $this->output, new ConfirmationQuestion($question, $answer)); } if ($answer) { $question .= " ($answer)"; } return $dialog->ask($this->input, $this->output, new Question("$question ", $answer)); } /** * Print a message to console. * * ```php * say('Welcome to Setup'); * ``` * * * @param string $message */ protected function say($message = '') { $this->output->writeln($message); } /** * Print a successful message * @param $message */ protected function saySuccess($message) { $this->say(" $message "); } /** * Print warning message * @param $message */ protected function sayWarning($message) { $this->say(" $message "); } /** * Print info message * @param $message */ protected function sayInfo($message) { $this->say("> $message"); } /** * Create a helper class inside a directory * * @param $name * @param $directory */ protected function createHelper($name, $directory) { $file = $this->createDirectoryFor( $dir = $directory . DIRECTORY_SEPARATOR . "Helper", "$name.php" ) . "$name.php"; $gen = new Lib\Generator\Helper($name, $this->namespace); // generate helper $this->createFile( $file, $gen->produce() ); require_once $file; $this->sayInfo("$name helper has been created in $dir"); } /** * Create an empty directory and add a placeholder file into it * @param $dir */ protected function createEmptyDirectory($dir) { $this->createDirectoryFor($dir); $this->createFile($dir . DIRECTORY_SEPARATOR . '.gitkeep', ''); } protected function gitIgnore($path) { if (file_exists(self::GIT_IGNORE)) { file_put_contents($path . DIRECTORY_SEPARATOR . self::GIT_IGNORE, "*\n!" . self::GIT_IGNORE); } } protected function checkInstalled($dir = '.') { if (file_exists($dir . DIRECTORY_SEPARATOR . 'codeception.yml') || file_exists($dir . DIRECTORY_SEPARATOR . 'codeception.dist.yml')) { throw new \Exception("Codeception is already installed in this directory"); } } /** * Create an Actor class and generate actions for it. * Requires a suite config as array in 3rd parameter. * * @param $name * @param $directory * @param $suiteConfig */ protected function createActor($name, $directory, $suiteConfig) { $file = $this->createDirectoryFor( $directory, $name ) . $this->getShortClassName($name); $file .= '.php'; $suiteConfig['namespace'] = $this->namespace; $config = Configuration::mergeConfigs(Configuration::$defaultSuiteSettings, $suiteConfig); $actorGenerator = new Lib\Generator\Actor($config); $content = $actorGenerator->produce(); $this->createFile($file, $content); $this->sayInfo("$name actor has been created in $directory"); $actionsGenerator = new Lib\Generator\Actions($config); $content = $actionsGenerator->produce(); $generatedDir = $directory . DIRECTORY_SEPARATOR . '_generated'; $this->createDirectoryFor($generatedDir, 'Actions.php'); $this->createFile($generatedDir . DIRECTORY_SEPARATOR . $actorGenerator->getActorName() . 'Actions.php', $content); $this->sayInfo("Actions have been loaded"); } } comment('I expect to ' . $prediction); } public function expect($prediction) { return $this->comment('I expect ' . $prediction); } public function amGoingTo($argumentation) { return $this->comment('I am going to ' . $argumentation); } public function am($role) { $role = trim($role); if (stripos('aeiou', $role[0]) !== false) { return $this->comment('As an ' . $role); } return $this->comment('As a ' . $role); } public function lookForwardTo($achieveValue) { return $this->comment('So that I ' . $achieveValue); } public function comment($description) { $this->getScenario()->comment($description); return $this; } } friends[$name])) { $actor = $actorClass === null ? $this : new $actorClass($this->getScenario()); $this->friends[$name] = new LibFriend($name, $actor, $this->getScenario()->current('modules')); } return $this->friends[$name]; } } setAutocompleter( new \Hoa\Console\Readline\Autocompleter\Word(get_class_methods($this)) ); $output = new ConsoleOutput(); $output->writeln(" Execution PAUSED, starting interactive shell..."); $output->writeln(" Type in commands to try them, ENTER to continue, TAB to auto-complete"); $result = ''; do { $command = $readline->readLine('$I->'); // “> ” is the prefix of the line. if ($command == 'exit') { return; } if ($command == '') { return; } try { $value = eval("return \$I->$command;"); if ($value) { $result = $value; if (!is_object($result)) { codecept_debug($result); } codecept_debug('>> Result saved to $result variable, you can use it in next commands'); } } catch (\PHPUnit\Framework\AssertionFailedError $fail) { $output->writeln("fail " . $fail->getMessage()); } catch (\Exception $e) { $output->writeln("error " . $e->getMessage()); } catch (\Throwable $e) { $output->writeln("syntax error " . $e->getMessage()); } } while (true); } } retryXXX() methods; * * @param $num * @param int $interval */ public function retry($num, $interval = 200) { $this->retryNum = $num; $this->retryInterval = $interval; } } false, 'headers' => [], ]; protected $refreshMaxInterval = 0; protected $awsCredentials = null; protected $awsSignature = null; /** @var \GuzzleHttp\Client */ protected $client; /** * Sets the maximum allowable timeout interval for a meta tag refresh to * automatically redirect a request. * * A meta tag detected with an interval equal to or greater than $seconds * would not result in a redirect. A meta tag without a specified interval * or one with a value less than $seconds would result in the client * automatically redirecting to the specified URL * * @param int $seconds Number of seconds */ public function setRefreshMaxInterval($seconds) { $this->refreshMaxInterval = $seconds; } public function setClient(GuzzleClient &$client) { $this->client = $client; } /** * Sets the request header to the passed value. The header will be * sent along with the next request. * * Passing an empty value clears the header, which is the equivalent * of calling deleteHeader. * * @param string $name the name of the header * @param string $value the value of the header */ public function setHeader($name, $value) { if (strval($value) === '') { $this->deleteHeader($name); } else { $this->requestOptions['headers'][$name] = $value; } } /** * Deletes the header with the passed name from the list of headers * that will be sent with the request. * * @param string $name the name of the header to delete. */ public function deleteHeader($name) { unset($this->requestOptions['headers'][$name]); } /** * @param string $username * @param string $password * @param string $type Default: 'basic' */ public function setAuth($username, $password, $type = 'basic') { if (!$username) { unset($this->requestOptions['auth']); return; } $this->requestOptions['auth'] = [$username, $password, $type]; } /** * Taken from Mink\BrowserKitDriver * * @param Response $response * * @return \Symfony\Component\BrowserKit\Response */ protected function createResponse(Psr7Response $response) { $body = (string) $response->getBody(); $headers = $response->getHeaders(); $contentType = null; if (isset($headers['Content-Type'])) { $contentType = reset($headers['Content-Type']); } if (!$contentType) { $contentType = 'text/html'; } if (strpos($contentType, 'charset=') === false) { if (preg_match('/\]+charset *= *["\']?([a-zA-Z\-0-9]+)/i', $body, $matches)) { $contentType .= ';charset=' . $matches[1]; } $headers['Content-Type'] = [$contentType]; } $status = $response->getStatusCode(); if ($status < 300 || $status >= 400) { $matches = []; $matchesMeta = preg_match( '/\]+http-equiv="refresh" content="\s*(\d*)\s*;\s*url=(.*?)"/i', $body, $matches ); if (!$matchesMeta && isset($headers['Refresh'])) { // match by header preg_match( '/^\s*(\d*)\s*;\s*url=(.*)/i', (string) reset($headers['Refresh']), $matches ); } if ((!empty($matches)) && (empty($matches[1]) || $matches[1] < $this->refreshMaxInterval)) { $uri = new Psr7Uri($this->getAbsoluteUri($matches[2])); $currentUri = new Psr7Uri($this->getHistory()->current()->getUri()); if ($uri->withFragment('') != $currentUri->withFragment('')) { $status = 302; $headers['Location'] = $matchesMeta ? htmlspecialchars_decode($uri) : (string)$uri; } } } return new BrowserKitResponse($body, $status, $headers); } public function getAbsoluteUri($uri) { $baseUri = $this->client->getConfig('base_uri'); if (strpos($uri, '://') === false && strpos($uri, '//') !== 0) { if (strpos($uri, '/') === 0) { $baseUriPath = $baseUri->getPath(); if (!empty($baseUriPath) && strpos($uri, $baseUriPath) === 0) { $uri = substr($uri, strlen($baseUriPath)); } return Uri::appendPath((string)$baseUri, $uri); } // relative url if (!$this->getHistory()->isEmpty()) { return Uri::mergeUrls((string)$this->getHistory()->current()->getUri(), $uri); } } return Uri::mergeUrls($baseUri, $uri); } protected function doRequest($request) { /** @var $request BrowserKitRequest **/ $guzzleRequest = new Psr7Request( $request->getMethod(), $request->getUri(), $this->extractHeaders($request), $request->getContent() ); $options = $this->requestOptions; $options['cookies'] = $this->extractCookies($guzzleRequest->getUri()->getHost()); $multipartData = $this->extractMultipartFormData($request); if (!empty($multipartData)) { $options['multipart'] = $multipartData; } $formData = $this->extractFormData($request); if (empty($multipartData) and $formData) { $options['form_params'] = $formData; } try { if (null !== $this->awsCredentials) { $response = $this->client->send($this->awsSignature->signRequest($guzzleRequest, $this->awsCredentials), $options); } else { $response = $this->client->send($guzzleRequest, $options); } } catch (RequestException $e) { if (!$e->hasResponse()) { throw $e; } $response = $e->getResponse(); } return $this->createResponse($response); } protected function extractHeaders(BrowserKitRequest $request) { $headers = []; $server = $request->getServer(); $contentHeaders = ['Content-Length' => true, 'Content-Md5' => true, 'Content-Type' => true]; foreach ($server as $header => $val) { $header = html_entity_decode(implode('-', array_map('ucfirst', explode('-', strtolower(str_replace('_', '-', $header))))), ENT_NOQUOTES); if (strpos($header, 'Http-') === 0) { $headers[substr($header, 5)] = $val; } elseif (isset($contentHeaders[$header])) { $headers[$header] = $val; } } return $headers; } protected function extractFormData(BrowserKitRequest $request) { if (!in_array(strtoupper($request->getMethod()), ['POST', 'PUT', 'PATCH', 'DELETE'])) { return null; } // guessing if it is a form data $headers = $request->getServer(); if (isset($headers['HTTP_CONTENT_TYPE'])) { // not a form if ($headers['HTTP_CONTENT_TYPE'] !== 'application/x-www-form-urlencoded') { return null; } } if ($request->getContent() !== null) { return null; } return $request->getParameters(); } protected function extractMultipartFormData(Request $request) { if (!in_array(strtoupper($request->getMethod()), ['POST', 'PUT', 'PATCH'])) { return []; } $parts = $this->mapFiles($request->getFiles()); if (empty($parts)) { return []; } foreach ($request->getParameters() as $k => $v) { $parts = $this->formatMultipart($parts, $k, $v); } return $parts; } protected function formatMultipart($parts, $key, $value) { if (is_array($value)) { foreach ($value as $subKey => $subValue) { $parts = array_merge($this->formatMultipart([], $key."[$subKey]", $subValue), $parts); } return $parts; } $parts[] = ['name' => $key, 'contents' => (string) $value]; return $parts; } protected function mapFiles($requestFiles, $arrayName = '') { $files = []; foreach ($requestFiles as $name => $info) { if (!empty($arrayName)) { $name = $arrayName . '[' . $name . ']'; } if (is_array($info)) { if (isset($info['tmp_name'])) { if ($info['tmp_name']) { $handle = fopen($info['tmp_name'], 'r'); $filename = isset($info['name']) ? $info['name'] : null; $file = [ 'name' => $name, 'contents' => $handle, 'filename' => $filename ]; if (isset($info['type'])) { $file['headers'] = [ 'content-type' => $info['type'] ]; } $files[] = $file; } } else { $files = array_merge($files, $this->mapFiles($info, $name)); } } else { $files[] = [ 'name' => $name, 'contents' => fopen($info, 'r') ]; } } return $files; } protected function extractCookies($host) { $jar = []; $cookies = $this->getCookieJar()->all(); foreach ($cookies as $cookie) { /** @var $cookie Cookie **/ $setCookie = SetCookie::fromString((string)$cookie); if (!$setCookie->getDomain()) { $setCookie->setDomain($host); } $jar[] = $setCookie; } return new CookieJar(false, $jar); } public static function createHandler($handler) { if ($handler instanceof HandlerStack) { return $handler; } if ($handler === 'curl') { return HandlerStack::create(new CurlHandler()); } if ($handler === 'stream') { return HandlerStack::create(new StreamHandler()); } if (is_string($handler) && class_exists($handler)) { return HandlerStack::create(new $handler); } if (is_callable($handler)) { return HandlerStack::create($handler); } return HandlerStack::create(); } public function setAwsAuth($config) { $this->awsCredentials = new Credentials($config['key'], $config['secret']); $this->awsSignature = new SignatureV4($config['service'], $config['region']); } } laravelExceptionHandler = $laravelExceptionHandler; } /** * @param boolean $exceptionHandlingDisabled */ public function exceptionHandlingDisabled($exceptionHandlingDisabled) { $this->exceptionHandlingDisabled = $exceptionHandlingDisabled; } /** * Report or log an exception. * * @param \Exception $e * @return void */ public function report(Exception $e) { $this->laravelExceptionHandler->report($e); } /** * Determine if the exception should be reported. * * @param \Exception $e * @return bool */ public function shouldReport(Exception $e) { return $this->exceptionHandlingDisabled; } /** * @param $request * @param Exception $e * @return \Symfony\Component\HttpFoundation\Response * @throws Exception */ public function render($request, Exception $e) { $response = $this->laravelExceptionHandler->render($request, $e); if ($this->exceptionHandlingDisabled && $this->isSymfonyExceptionHandlerOutput($response->getContent())) { // If content was generated by the \Symfony\Component\Debug\ExceptionHandler class // the Laravel application could not handle the exception, // so re-throw this exception if the Codeception user disabled Laravel's exception handling. throw $e; } return $response; } /** * Check if the response content is HTML output of the Symfony exception handler class. * * @param string $content * @return bool */ private function isSymfonyExceptionHandlerOutput($content) { return strpos($content, '

') !== false || strpos($content, '
') !== false; } /** * Render an exception to the console. * * @param \Symfony\Component\Console\Output\OutputInterface $output * @param \Exception $e * @return void */ public function renderForConsole($output, Exception $e) { $this->laravelExceptionHandler->renderForConsole($output, $e); } /** * @param string $method * @param array $args * @return mixed */ public function __call($method, $args) { return call_user_func_array([$this->laravelExceptionHandler, $method], $args); } } module = $module; $this->exceptionHandlingDisabled = $this->module->config['disable_exception_handling']; $this->middlewareDisabled = $this->module->config['disable_middleware']; $this->eventsDisabled = $this->module->config['disable_events']; $this->modelEventsDisabled = $this->module->config['disable_model_events']; $this->initialize(); $components = parse_url($this->app['config']->get('app.url', 'http://localhost')); if (array_key_exists('url', $this->module->config)) { $components = parse_url($this->module->config['url']); } $host = isset($components['host']) ? $components['host'] : 'localhost'; parent::__construct($this->app, ['HTTP_HOST' => $host]); // Parent constructor defaults to not following redirects $this->followRedirects(true); } /** * Execute a request. * * @param SymfonyRequest $request * @return Response */ protected function doRequest($request) { if (!$this->firstRequest) { $this->initialize($request); } $this->firstRequest = false; $this->applyBindings(); $this->applyContextualBindings(); $this->applyInstances(); $this->applyApplicationHandlers(); $request = Request::createFromBase($request); $response = $this->kernel->handle($request); $this->app->make('Illuminate\Contracts\Http\Kernel')->terminate($request, $response); return $response; } /** * Make sure files are \Illuminate\Http\UploadedFile instances with the private $test property set to true. * Fixes issue https://github.com/Codeception/Codeception/pull/3417. * * @param array $files * @return array */ protected function filterFiles(array $files) { $files = parent::filterFiles($files); if (! class_exists('Illuminate\Http\UploadedFile')) { // The \Illuminate\Http\UploadedFile class was introduced in Laravel 5.2.15, // so don't change the $files array if it does not exist. return $files; } return $this->convertToTestFiles($files); } /** * @param array $files * @return array */ private function convertToTestFiles(array $files) { $filtered = []; foreach ($files as $key => $value) { if (is_array($value)) { $filtered[$key] = $this->convertToTestFiles($value); } else { $filtered[$key] = UploadedFile::createFromBase($value, true); } } return $filtered; } /** * Initialize the Laravel framework. * * @param SymfonyRequest $request */ private function initialize($request = null) { // Store a reference to the database object // so the database connection can be reused during tests $this->oldDb = null; if (isset($this->app['db']) && $this->app['db']->connection()) { $this->oldDb = $this->app['db']; } $this->app = $this->kernel = $this->loadApplication(); // Set the request instance for the application, if (is_null($request)) { $appConfig = require $this->module->config['project_dir'] . 'config/app.php'; $request = SymfonyRequest::create($appConfig['url']); } $this->app->instance('request', Request::createFromBase($request)); // Reset the old database after all the service providers are registered. if ($this->oldDb) { $this->app['events']->listen('bootstrapped: Illuminate\Foundation\Bootstrap\RegisterProviders', function () { $this->app->singleton('db', function () { return $this->oldDb; }); }); } $this->app->make('Illuminate\Contracts\Http\Kernel')->bootstrap(); // Record all triggered events by adding a wildcard event listener // Since Laravel 5.4 wildcard event handlers receive the event name as the first argument, // but for earlier Laravel versions the firing() method of the event dispatcher should be used // to determine the event name. if (method_exists($this->app['events'], 'firing')) { $listener = function () { $this->triggeredEvents[] = $this->normalizeEvent($this->app['events']->firing()); }; } else { $listener = function ($event) { $this->triggeredEvents[] = $this->normalizeEvent($event); }; } $this->app['events']->listen('*', $listener); // Replace the Laravel exception handler with our decorated exception handler, // so exceptions can be intercepted for the disable_exception_handling functionality. $decorator = new ExceptionHandlerDecorator($this->app['Illuminate\Contracts\Debug\ExceptionHandler']); $decorator->exceptionHandlingDisabled($this->exceptionHandlingDisabled); $this->app->instance('Illuminate\Contracts\Debug\ExceptionHandler', $decorator); if ($this->module->config['disable_middleware'] || $this->middlewareDisabled) { $this->app->instance('middleware.disable', true); } if ($this->module->config['disable_events'] || $this->eventsDisabled) { $this->mockEventDispatcher(); } if ($this->module->config['disable_model_events'] || $this->modelEventsDisabled) { Model::unsetEventDispatcher(); } $this->module->setApplication($this->app); } /** * Boot the Laravel application object. * @return Application * @throws ModuleConfig */ private function loadApplication() { $app = require $this->module->config['bootstrap_file']; $app->loadEnvironmentFrom($this->module->config['environment_file']); $app->instance('request', new Request()); return $app; } /** * Replace the Laravel event dispatcher with a mock. */ private function mockEventDispatcher() { // Even if events are disabled we still want to record the triggered events. // But by mocking the event dispatcher the wildcard listener registered in the initialize method is removed. // So to record the triggered events we have to catch the calls to the fire method of the event dispatcher mock. $callback = function ($event) { $this->triggeredEvents[] = $this->normalizeEvent($event); return []; }; // In Laravel 5.4 the Illuminate\Contracts\Events\Dispatcher interface was changed, // the 'fire' method was renamed to 'dispatch'. This code determines the correct method to mock. $method = method_exists($this->app['events'], 'dispatch') ? 'dispatch' : 'fire'; $mock = Stub::makeEmpty('Illuminate\Contracts\Events\Dispatcher', [ $method => $callback ]); $this->app->instance('events', $mock); } /** * Normalize events to class names. * * @param $event * @return string */ private function normalizeEvent($event) { if (is_object($event)) { $event = get_class($event); } if (preg_match('/^bootstrapp(ing|ed): /', $event)) { return $event; } // Events can be formatted as 'event.name: parameters' $segments = explode(':', $event); return $segments[0]; } //====================================================================== // Public methods called by module //====================================================================== /** * Did an event trigger? * * @param $event * @return bool */ public function eventTriggered($event) { $event = $this->normalizeEvent($event); foreach ($this->triggeredEvents as $triggeredEvent) { if ($event == $triggeredEvent || is_subclass_of($event, $triggeredEvent)) { return true; } } return false; } /** * Disable Laravel exception handling. */ public function disableExceptionHandling() { $this->exceptionHandlingDisabled = true; $this->app['Illuminate\Contracts\Debug\ExceptionHandler']->exceptionHandlingDisabled(true); } /** * Enable Laravel exception handling. */ public function enableExceptionHandling() { $this->exceptionHandlingDisabled = false; $this->app['Illuminate\Contracts\Debug\ExceptionHandler']->exceptionHandlingDisabled(false); } /** * Disable events. */ public function disableEvents() { $this->eventsDisabled = true; $this->mockEventDispatcher(); } /** * Disable model events. */ public function disableModelEvents() { $this->modelEventsDisabled = true; Model::unsetEventDispatcher(); } /* * Disable middleware. */ public function disableMiddleware() { $this->middlewareDisabled = true; $this->app->instance('middleware.disable', true); } } module = $module; $components = parse_url($this->module->config['url']); $server = ['HTTP_HOST' => $components['host']]; // Pass a DummyKernel to satisfy the arguments of the parent constructor. // The actual kernel object is set in the initialize() method. parent::__construct(new DummyKernel(), $server); // Parent constructor defaults to not following redirects $this->followRedirects(true); $this->initialize(); } /** * Execute a request. * * @param SymfonyRequest $request * @return Response */ protected function doRequest($request) { if (!$this->firstRequest) { $this->initialize($request); } $this->firstRequest = false; $this->applyBindings(); $this->applyContextualBindings(); $this->applyInstances(); $this->applyApplicationHandlers(); $request = Request::createFromBase($request); $response = $this->kernel->handle($request); $method = new \ReflectionMethod(get_class($this->app), 'callTerminableMiddleware'); $method->setAccessible(true); $method->invoke($this->app, $response); return $response; } /** * Initialize the Lumen framework. * * @param SymfonyRequest|null $request */ private function initialize($request = null) { // Store a reference to the database object // so the database connection can be reused during tests $this->oldDb = null; if (isset($this->app['db']) && $this->app['db']->connection()) { $this->oldDb = $this->app['db']; } if (class_exists(Facade::class)) { // If the container has been instantiated ever, // we need to clear its static fields before create new container. Facade::clearResolvedInstances(); } $this->app = $this->kernel = require $this->module->config['bootstrap_file']; // in version 5.6.*, lumen introduced the same design pattern like Laravel // to load all service provider we need to call on Laravel\Lumen\Application::boot() if (method_exists($this->app, 'boot')) { $this->app->boot(); } // Lumen registers necessary bindings on demand when calling $app->make(), // so here we force the request binding before registering our own request object, // otherwise Lumen will overwrite our request object. $this->app->make('request'); $request = $request ?: SymfonyRequest::create($this->module->config['url']); $this->app->instance('Illuminate\Http\Request', Request::createFromBase($request)); // Reset the old database if there is one if ($this->oldDb) { $this->app->singleton('db', function () { return $this->oldDb; }); Model::setConnectionResolver($this->oldDb); } $this->module->setApplication($this->app); } /** * Make sure files are \Illuminate\Http\UploadedFile instances with the private $test property set to true. * Fixes issue https://github.com/Codeception/Codeception/pull/3417. * * @param array $files * @return array */ protected function filterFiles(array $files) { $files = parent::filterFiles($files); if (! class_exists('Illuminate\Http\UploadedFile')) { // The \Illuminate\Http\UploadedFile class was introduced in Laravel 5.2.15, // so don't change the $files array if it does not exist. return $files; } return $this->convertToTestFiles($files); } /** * @param array $files * @return array */ private function convertToTestFiles(array $files) { $filtered = []; foreach ($files as $key => $value) { if (is_array($value)) { $filtered[$key] = $this->convertToTestFiles($value); } else { $filtered[$key] = UploadedFile::createFromBase($value, true); } } return $filtered; } } sessionId = $this->generateId(); if (is_array($options)) { $this->setOptions($options); } } /** * @inheritdoc */ public function start() { if ($this->status() !== PHP_SESSION_ACTIVE) { $this->memory = []; $this->started = true; return true; } return false; } /** * @inheritdoc * * @param array $options */ public function setOptions(array $options) { if (isset($options['uniqueId'])) { $this->sessionId = $options['uniqueId']; } $this->options = $options; } /** * @inheritdoc * * @return array */ public function getOptions() { return $this->options; } /** * @inheritdoc * * @param string $index * @param mixed $defaultValue * @param bool $remove * @return mixed */ public function get($index, $defaultValue = null, $remove = false) { $key = $this->prepareIndex($index); if (!isset($this->memory[$key])) { return $defaultValue; } $return = $this->memory[$key]; if ($remove) { unset($this->memory[$key]); } return $return; } /** * @inheritdoc * * @param string $index * @param mixed $value */ public function set($index, $value) { $this->memory[$this->prepareIndex($index)] = $value; } /** * @inheritdoc * * @param string $index * @return bool */ public function has($index) { return isset($this->memory[$this->prepareIndex($index)]); } /** * @inheritdoc * * @param string $index */ public function remove($index) { unset($this->memory[$this->prepareIndex($index)]); } /** * @inheritdoc * * @return string */ public function getId() { return $this->sessionId; } /** * @inheritdoc * * @return bool */ public function isStarted() { return $this->started; } /** * Returns the status of the current session * * ``` php * status() !== PHP_SESSION_ACTIVE) { * $session->start(); * } * ?> * ``` * * @return int */ public function status() { if ($this->isStarted()) { return PHP_SESSION_ACTIVE; } return PHP_SESSION_NONE; } /** * @inheritdoc * * @param bool $removeData * @return bool */ public function destroy($removeData = false) { if ($removeData) { if (!empty($this->sessionId)) { foreach ($this->memory as $key => $value) { if (0 === strpos($key, $this->sessionId . '#')) { unset($this->memory[$key]); } } } else { $this->memory = []; } } $this->started = false; return true; } /** * @inheritdoc * * @param bool $deleteOldSession * @return \Phalcon\Session\AdapterInterface */ public function regenerateId($deleteOldSession = true) { $this->sessionId = $this->generateId(); return $this; } /** * @inheritdoc * * @param string $name */ public function setName($name) { $this->name = $name; } /** * @inheritdoc * * @return string */ public function getName() { return $this->name; } /** * Dump all session * * @return array */ public function toArray() { return (array) $this->memory; } /** * Alias: Gets a session variable from an application context * * @param string $index * @return mixed */ public function __get($index) { return $this->get($index); } /** * Alias: Sets a session variable in an application context * * @param string $index * @param mixed $value */ public function __set($index, $value) { $this->set($index, $value); } /** * Alias: Check whether a session variable is set in an application context * * @param string $index * @return bool */ public function __isset($index) { return $this->has($index); } /** * Alias: Removes a session variable from an application context * * @param string $index */ public function __unset($index) { $this->remove($index); } private function prepareIndex($index) { if ($this->sessionId) { $key = $this->sessionId . '#' . $index; } else { $key = $index; } return $key; } /** * @return string */ private function generateId() { return md5(time()); } } application = $application; } /** * Get Phalcon Application * * @return Application|MicroApplication */ public function getApplication() { $application = $this->application; if ($application instanceof Closure) { return $application(); } elseif (is_string($application)) { /** @noinspection PhpIncludeInspection */ return require $application; } return $application; } /** * Makes a request. * * @param \Symfony\Component\BrowserKit\Request $request * * @return \Symfony\Component\BrowserKit\Response * @throws \RuntimeException */ public function doRequest($request) { $application = $this->getApplication(); if (!$application instanceof Application && !$application instanceof MicroApplication) { throw new RuntimeException('Unsupported application class.'); } $di = $application->getDI(); /** @var Http\Request $phRequest */ if ($di->has('request')) { $phRequest = $di->get('request'); } if (!$phRequest instanceof Http\RequestInterface) { $phRequest = new Http\Request(); } $uri = $request->getUri() ?: $phRequest->getURI(); $pathString = parse_url($uri, PHP_URL_PATH); $queryString = parse_url($uri, PHP_URL_QUERY); $_SERVER = $request->getServer(); $_SERVER['REQUEST_METHOD'] = strtoupper($request->getMethod()); $_SERVER['REQUEST_URI'] = null === $queryString ? $pathString : $pathString . '?' . $queryString; $_COOKIE = $request->getCookies(); $_FILES = $this->remapFiles($request->getFiles()); $_REQUEST = $this->remapRequestParameters($request->getParameters()); $_POST = []; $_GET = []; if ($_SERVER['REQUEST_METHOD'] == 'GET') { $_GET = $_REQUEST; } else { $_POST = $_REQUEST; } parse_str($queryString, $output); foreach ($output as $k => $v) { $_GET[$k] = $v; } $_GET['_url'] = $pathString; $_SERVER['QUERY_STRING'] = http_build_query($_GET); Di::reset(); Di::setDefault($di); $di['request'] = Stub::construct($phRequest, [], ['getRawBody' => $request->getContent()]); $response = $application->handle(); if (!$response instanceof Http\ResponseInterface) { $response = $application->response; } $headers = $response->getHeaders(); $status = (int) $headers->get('Status'); $headersProperty = new ReflectionProperty($headers, '_headers'); $headersProperty->setAccessible(true); $headers = $headersProperty->getValue($headers); if (!is_array($headers)) { $headers = []; } $cookiesProperty = new ReflectionProperty($di['cookies'], '_cookies'); $cookiesProperty->setAccessible(true); $cookies = $cookiesProperty->getValue($di['cookies']); if (is_array($cookies)) { $restoredProperty = new ReflectionProperty('\Phalcon\Http\Cookie', '_restored'); $restoredProperty->setAccessible(true); $valueProperty = new ReflectionProperty('\Phalcon\Http\Cookie', '_value'); $valueProperty->setAccessible(true); foreach ($cookies as $name => $cookie) { if (!$restoredProperty->getValue($cookie)) { $clientCookie = new Cookie( $name, $valueProperty->getValue($cookie), $cookie->getExpiration(), $cookie->getPath(), $cookie->getDomain(), $cookie->getSecure(), $cookie->getHttpOnly() ); $headers['Set-Cookie'][] = (string)$clientCookie; } } } return new Response( $response->getContent() ?: '', $status ? $status : 200, $headers ); } } applicationHandlers as $handler) { call_user_func($handler, $this->app); } } /** * Apply the registered Laravel service container bindings. */ private function applyBindings() { foreach ($this->bindings as $abstract => $binding) { list($concrete, $shared) = $binding; $this->app->bind($abstract, $concrete, $shared); } } /** * Apply the registered Laravel service container contextual bindings. */ private function applyContextualBindings() { foreach ($this->contextualBindings as $concrete => $bindings) { foreach ($bindings as $abstract => $implementation) { $this->app->addContextualBinding($concrete, $abstract, $implementation); } } } /** * Apply the registered Laravel service container instance bindings. */ private function applyInstances() { foreach ($this->instances as $abstract => $instance) { $this->app->instance($abstract, $instance); } } //====================================================================== // Public methods called by module //====================================================================== /** * Register a Laravel service container binding that should be applied * after initializing the Laravel Application object. * * @param $abstract * @param $concrete * @param bool $shared */ public function haveBinding($abstract, $concrete, $shared = false) { $this->bindings[$abstract] = [$concrete, $shared]; } /** * Register a Laravel service container contextual binding that should be applied * after initializing the Laravel Application object. * * @param $concrete * @param $abstract * @param $implementation */ public function haveContextualBinding($concrete, $abstract, $implementation) { if (! isset($this->contextualBindings[$concrete])) { $this->contextualBindings[$concrete] = []; } $this->contextualBindings[$concrete][$abstract] = $implementation; } /** * Register a Laravel service container instance binding that should be applied * after initializing the Laravel Application object. * * @param $abstract * @param $instance */ public function haveInstance($abstract, $instance) { $this->instances[$abstract] = $instance; } /** * Register a handler than can be used to modify the Laravel application object after it is initialized. * The Laravel application object will be passed as an argument to the handler. * * @param $handler */ public function haveApplicationHandler($handler) { $this->applicationHandlers[] = $handler; } /** * Clear the registered application handlers. */ public function clearApplicationHandlers() { $this->applicationHandlers = []; } } rearrangeFiles($requestFiles); return $this->replaceSpaces($files); } /** * Escape high-level variable name with dots, underscores and other "special" chars * to be compatible with PHP "bug" * @see https://bugs.php.net/bug.php?id=40000 * * @param array $parameters * * @return array */ protected function remapRequestParameters(array $parameters) { return $this->replaceSpaces($parameters); } private function rearrangeFiles($requestFiles) { $files = []; foreach ($requestFiles as $name => $info) { if (!is_array($info)) { continue; } /** * If we have a form with fields like * ``` * * * ``` * then only array variable will be used while simple variable will be ignored in php $_FILES * (eg $_FILES = [ * foo => [ * tmp_name => [ * 'bar' => 'asdf' * ], * //... * ] * ] * ) * (notice there is no entry for file "foo", only for file "foo[bar]" * this will check if current element contains inner arrays within it's keys * so we can ignore element itself and only process inner files */ $hasInnerArrays = count(array_filter($info, 'is_array')); if ($hasInnerArrays || !isset($info['tmp_name'])) { $inner = $this->remapFiles($info); foreach ($inner as $innerName => $innerInfo) { /** * Convert from ['a' => ['tmp_name' => '/tmp/test.txt'] ] * to ['tmp_name' => ['a' => '/tmp/test.txt'] ] */ $innerInfo = array_map( function ($v) use ($innerName) { return [$innerName => $v]; }, $innerInfo ); if (empty($files[$name])) { $files[$name] = []; } $files[$name] = array_replace_recursive($files[$name], $innerInfo); } } else { $files[$name] = $info; } } return $files; } /** * Replace spaces and dots and other chars in high-level query parameters for * compatibility with PHP bug (or not a bug) * @see https://bugs.php.net/bug.php?id=40000 * * @param array $parameters Array of request parameters to be converted * * @return array */ private function replaceSpaces($parameters) { $qs = http_build_query($parameters, '', '&'); parse_str($qs, $output); return $output; } } followRedirects(true); $this->rebootable = (boolean)$rebootable; $this->persistentServices = $services; $this->container = $this->kernel->getContainer(); $this->rebootKernel(); } /** * @param \Symfony\Component\HttpFoundation\Request $request */ protected function doRequest($request) { if ($this->rebootable) { if ($this->hasPerformedRequest) { $this->rebootKernel(); } else { $this->hasPerformedRequest = true; } } return parent::doRequest($request); } /** * Reboot kernel * * Services from the list of persistent services * are updated from service container before kernel shutdown * and injected into newly initialized container after kernel boot. */ public function rebootKernel() { if ($this->container) { foreach ($this->persistentServices as $serviceName => $service) { if ($this->container->has($serviceName)) { $this->persistentServices[$serviceName] = $this->container->get($serviceName); } } } $this->kernel->shutdown(); $this->kernel->boot(); $this->container = $this->kernel->getContainer(); foreach ($this->persistentServices as $serviceName => $service) { try { $this->container->set($serviceName, $service); } catch (\InvalidArgumentException $e) { //Private services can't be set in Symfony 4 codecept_debug("[Symfony] Can't set persistent service $serviceName: " . $e->getMessage()); } } if ($this->container->has('profiler')) { $this->container->get('profiler')->enable(); } } } index = $index; } public function mockResponse($response) { $this->mockedResponse = $response; } public function doRequest($request) { if ($this->mockedResponse) { $response = $this->mockedResponse; $this->mockedResponse = null; return $response; } $_COOKIE = $request->getCookies(); $_SERVER = $request->getServer(); $_FILES = $this->remapFiles($request->getFiles()); $uri = str_replace('http://localhost', '', $request->getUri()); $_REQUEST = $this->remapRequestParameters($request->getParameters()); if (strtoupper($request->getMethod()) == 'GET') { $_GET = $_REQUEST; } else { $_POST = $_REQUEST; } $_SERVER['REQUEST_METHOD'] = strtoupper($request->getMethod()); $_SERVER['REQUEST_URI'] = $uri; ob_start(); include $this->index; $content = ob_get_contents(); ob_end_clean(); $headers = []; $php_headers = headers_list(); foreach ($php_headers as $value) { // Get the header name $parts = explode(':', $value); if (count($parts) > 1) { $name = trim(array_shift($parts)); // Build the header hash map $headers[$name] = trim(implode(':', $parts)); } } $headers['Content-type'] = isset($headers['Content-type']) ? $headers['Content-type'] : "text/html; charset=UTF-8"; $response = new Response($content, 200, $headers); return $response; } } headers = []; $_COOKIE = array_merge($_COOKIE, $request->getCookies()); $_SERVER = array_merge($_SERVER, $request->getServer()); $_FILES = $this->remapFiles($request->getFiles()); $_REQUEST = $this->remapRequestParameters($request->getParameters()); $_POST = $_GET = []; if (strtoupper($request->getMethod()) == 'GET') { $_GET = $_REQUEST; } else { $_POST = $_REQUEST; } // Parse url parts $uriPath = ltrim(parse_url($request->getUri(), PHP_URL_PATH), '/'); $uriQuery = ltrim(parse_url($request->getUri(), PHP_URL_QUERY), '?'); $scriptName = trim(parse_url($this->url, PHP_URL_PATH), '/'); if (!empty($uriQuery)) { $uriPath .= "?{$uriQuery}"; parse_str($uriQuery, $params); foreach ($params as $k => $v) { $_GET[$k] = $v; } } // Add script name to request if none if ($scriptName and strpos($uriPath, $scriptName) === false) { $uriPath = "/{$scriptName}/{$uriPath}"; } // Add forward slash if not exists if (strpos($uriPath, '/') !== 0) { $uriPath = "/{$uriPath}"; } $_SERVER['REQUEST_METHOD'] = strtoupper($request->getMethod()); $_SERVER['REQUEST_URI'] = $uriPath; /** * Hack to be sure that CHttpRequest will resolve route correctly */ $_SERVER['SCRIPT_NAME'] = "/{$scriptName}"; $_SERVER['SCRIPT_FILENAME'] = $this->appPath; ob_start(); \Yii::setApplication(null); \Yii::createApplication($this->appSettings['class'], $this->appSettings['config']); $app = \Yii::app(); // disabling logging. Logs slow down test execution if ($app->hasComponent('log')) { foreach ($app->getComponent('log')->routes as $route) { $route->enabled = false; } } if ($app->hasComponent('session')) { // disable regenerate id in session $app->setComponent('session', Stub::make('CHttpSession', ['regenerateID' => false])); } $app->onEndRequest->add([$this, 'setHeaders']); $app->run(); if ($app->hasComponent('db')) { // close connection $app->getDb()->setActive(false); // cleanup metadata cache $property = new \ReflectionProperty('CActiveRecord', '_md'); $property->setAccessible(true); $property->setValue([]); } $content = ob_get_clean(); $headers = $this->getHeaders(); $statusCode = 200; foreach ($headers as $header => $val) { if ($header == 'Location') { $statusCode = 302; } } $response = new Response($content, $statusCode, $this->getHeaders()); return $response; } /** * Set current client headers when terminating yii application (onEndRequest) */ public function setHeaders() { $this->headers = \Yii::app()->request->getAllHeaders(); } /** * Returns current client headers * @return array headers */ public function getHeaders() { return $this->headers; } } handler = function (Event $event) { if ($event->sender instanceof Connection) { $this->connectionOpened($event->sender); } }; } protected function connectionOpened(Connection $connection) { $this->debug('Connection opened!'); if ($connection instanceof Connection) { $this->connections[] = $connection; } } public function start() { Event::on(Connection::class, Connection::EVENT_AFTER_OPEN, $this->handler); $this->debug('watching new connections'); } public function stop() { Event::off(Connection::class, Connection::EVENT_AFTER_OPEN, $this->handler); $this->debug('no longer watching new connections'); } public function closeAll() { $count = count($this->connections); $this->debug("closing all ($count) connections"); foreach ($this->connections as $connection) { $connection->close(); } } protected function debug($message) { $title = (new \ReflectionClass($this))->getShortName(); if (is_array($message) or is_object($message)) { $message = stripslashes(json_encode($message)); } codecept_debug("[$title] $message"); } } data = $data; } public function fixtures() { return $this->data; } public function globalFixtures() { return [ InitDbFixture::className() ]; } } __toString(); } Debug::debug("[$category] " . \yii\helpers\VarDumper::export($message)); } } callback, $message); return true; } protected function saveMessage($message) { call_user_func($this->callback, $message); return true; } } ignoreCollidingDSN = $ignoreCollidingDSN; } protected function connectionOpened(Connection $connection) { parent::connectionOpened($connection); /** * We should check if the known PDO objects are the same, in which case we should reuse the PDO * object so only 1 transaction is started and multiple connections to the same database see the * same data (due to writes inside a transaction not being visible from the outside). * */ $key = md5(json_encode([ 'dsn' => $connection->dsn, 'user' => $connection->username, 'pass' => $connection->password, 'attributes' => $connection->attributes, 'emulatePrepare' => $connection->emulatePrepare, 'charset' => $connection->charset ])); /* * If keys match we assume connections are "similar enough". */ if (isset($this->pdoCache[$key])) { $connection->pdo = $this->pdoCache[$key]; } else { $this->pdoCache[$key] = $connection->pdo; } if (isset($this->dsnCache[$connection->dsn]) && $this->dsnCache[$connection->dsn] !== $key && !$this->ignoreCollidingDSN ) { $this->debug(<<dsn}) with different configuration. These connections will not see the same database state since we cannot share a transaction between different PDO instances. You can remove this message by adding 'ignoreCollidingDSN = true' in the module configuration. TEXT ); Debug::pause(); } if (isset($this->transactions[$key])) { $this->debug('Reusing PDO, so no need for a new transaction'); return; } $this->debug('Transaction started for: ' . $connection->dsn); $this->transactions[$key] = $connection->beginTransaction(); } public function rollbackAll() { /** @var Transaction $transaction */ foreach ($this->transactions as $transaction) { if ($transaction->db->isActive) { $transaction->rollBack(); $this->debug('Transaction cancelled; all changes reverted.'); } } $this->transactions = []; $this->pdoCache = []; $this->dsnCache = []; } } startApp(); } return Yii::$app; } public function resetApplication() { codecept_debug('Destroying application'); $this->closeSession(); Yii::$app = null; \yii\web\UploadedFile::reset(); if (method_exists(\yii\base\Event::className(), 'offAll')) { \yii\base\Event::offAll(); } Yii::setLogger(null); // This resolves an issue with database connections not closing properly. gc_collect_cycles(); } /** * Finds and logs in a user * @internal * @param $user * @throws ConfigurationException * @throws \RuntimeException */ public function findAndLoginUser($user) { $app = $this->getApplication(); if (!$app->has('user')) { throw new ConfigurationException('The user component is not configured'); } if ($user instanceof \yii\web\IdentityInterface) { $identity = $user; } else { // class name implementing IdentityInterface $identityClass = $app->user->identityClass; $identity = call_user_func([$identityClass, 'findIdentity'], $user); if (!isset($identity)) { throw new \RuntimeException('User not found'); } } $app->user->login($identity); } /** * Masks a value * @internal * @param string $val * @return string * @see \yii\base\Security::maskToken */ public function maskToken($val) { return $this->getApplication()->security->maskToken($val); } /** * @internal * @param string $name The name of the cookie * @param string $value The value of the cookie * @return string The value to send to the browser */ public function hashCookieData($name, $value) { $app = $this->getApplication(); if (!$app->request->enableCookieValidation) { return $value; } return $app->security->hashData(serialize([$name, $value]), $app->request->cookieValidationKey); } /** * @internal * @return array List of regex patterns for recognized domain names */ public function getInternalDomains() { /** @var \yii\web\UrlManager $urlManager */ $urlManager = $this->getApplication()->urlManager; $domains = [$this->getDomainRegex($urlManager->hostInfo)]; if ($urlManager->enablePrettyUrl) { foreach ($urlManager->rules as $rule) { /** @var \yii\web\UrlRule $rule */ if (isset($rule->host)) { $domains[] = $this->getDomainRegex($rule->host); } } } return array_unique($domains); } /** * @internal * @return array List of sent emails */ public function getEmails() { return $this->emails; } /** * @internal */ public function getComponent($name) { $app = $this->getApplication(); if (!$app->has($name)) { throw new ConfigurationException("Component $name is not available in current application"); } return $app->get($name); } /** * Getting domain regex from rule host template * * @param string $template * @return string */ private function getDomainRegex($template) { if (preg_match('#https?://(.*)#', $template, $matches)) { $template = $matches[1]; } $parameters = []; if (strpos($template, '<') !== false) { $template = preg_replace_callback( '/<(?:\w+):?([^>]+)?>/u', function ($matches) use (&$parameters) { $key = '#' . count($parameters) . '#'; $parameters[$key] = isset($matches[1]) ? $matches[1] : '\w+'; return $key; }, $template ); } $template = preg_quote($template); $template = strtr($template, $parameters); return '/^' . $template . '$/u'; } /** * Gets the name of the CSRF param. * @internal * @return string */ public function getCsrfParamName() { return $this->getApplication()->request->csrfParam; } /** * @internal * @param $params * @return mixed */ public function createUrl($params) { return is_array($params) ?$this->getApplication()->getUrlManager()->createUrl($params) : $params; } public function startApp() { codecept_debug('Starting application'); $config = require($this->configFile); if (!isset($config['class'])) { $config['class'] = 'yii\web\Application'; } $config = $this->mockMailer($config); /** @var \yii\web\Application $app */ Yii::$app = Yii::createObject($config); Yii::setLogger(new Logger()); } /** * * @param \Symfony\Component\BrowserKit\Request $request * * @return \Symfony\Component\BrowserKit\Response */ public function doRequest($request) { $_COOKIE = $request->getCookies(); $_SERVER = $request->getServer(); $_FILES = $this->remapFiles($request->getFiles()); $_REQUEST = $this->remapRequestParameters($request->getParameters()); $_POST = $_GET = []; if (strtoupper($request->getMethod()) === 'GET') { $_GET = $_REQUEST; } else { $_POST = $_REQUEST; } $uri = $request->getUri(); $pathString = parse_url($uri, PHP_URL_PATH); $queryString = parse_url($uri, PHP_URL_QUERY); $_SERVER['REQUEST_URI'] = $queryString === null ? $pathString : $pathString . '?' . $queryString; $_SERVER['REQUEST_METHOD'] = strtoupper($request->getMethod()); parse_str($queryString, $params); foreach ($params as $k => $v) { $_GET[$k] = $v; } ob_start(); $this->beforeRequest(); $app = $this->getApplication(); // disabling logging. Logs are slowing test execution down foreach ($app->log->targets as $target) { $target->enabled = false; } $yiiRequest = $app->getRequest(); if ($request->getContent() !== null) { $yiiRequest->setRawBody($request->getContent()); $yiiRequest->setBodyParams(null); } else { $yiiRequest->setRawBody(null); $yiiRequest->setBodyParams($_POST); } $yiiRequest->setQueryParams($_GET); try { /* * This is basically equivalent to $app->run() without sending the response. * Sending the response is problematic because it tries to send headers. */ $app->trigger($app::EVENT_BEFORE_REQUEST); $response = $app->handleRequest($yiiRequest); $app->trigger($app::EVENT_AFTER_REQUEST); $response->send(); } catch (\Exception $e) { if ($e instanceof HttpException) { // Don't discard output and pass exception handling to Yii to be able // to expect error response codes in tests. $app->errorHandler->discardExistingOutput = false; $app->errorHandler->handleException($e); } elseif (!$e instanceof ExitException) { // for exceptions not related to Http, we pass them to Codeception throw $e; } $response = $app->response; } $this->encodeCookies($response, $yiiRequest, $app->security); if ($response->isRedirection) { Debug::debug("[Redirect with headers]" . print_r($response->getHeaders()->toArray(), true)); } $content = ob_get_clean(); if (empty($content) && !empty($response->content) && !isset($response->stream)) { throw new \Exception('No content was sent from Yii application'); } return new Response($content, $response->statusCode, $response->getHeaders()->toArray()); } protected function revertErrorHandler() { $handler = new ErrorHandler(); set_error_handler([$handler, 'errorHandler']); } /** * Encodes the cookies and adds them to the headers. * @param \yii\web\Response $response * @throws \yii\base\InvalidConfigException */ protected function encodeCookies( YiiResponse $response, Request $request, Security $security ) { if ($request->enableCookieValidation) { $validationKey = $request->cookieValidationKey; } foreach ($response->getCookies() as $cookie) { /** @var \yii\web\Cookie $cookie */ $value = $cookie->value; if ($cookie->expire != 1 && isset($validationKey)) { $data = version_compare(Yii::getVersion(), '2.0.2', '>') ? [$cookie->name, $cookie->value] : $cookie->value; $value = $security->hashData(serialize($data), $validationKey); } $c = new Cookie( $cookie->name, $value, $cookie->expire, $cookie->path, $cookie->domain, $cookie->secure, $cookie->httpOnly ); $this->getCookieJar()->set($c); } } /** * Replace mailer with in memory mailer * @param array $config Original configuration * @return array New configuration */ protected function mockMailer(array $config) { // options that make sense for mailer mock $allowedOptions = [ 'htmlLayout', 'textLayout', 'messageConfig', 'messageClass', 'useFileTransport', 'fileTransportPath', 'fileTransportCallback', 'view', 'viewPath', ]; $mailerConfig = [ 'class' => TestMailer::class, 'callback' => function (MessageInterface $message) { $this->emails[] = $message; } ]; if (isset($config['components']['mailer']) && is_array($config['components']['mailer'])) { foreach ($config['components']['mailer'] as $name => $value) { if (in_array($name, $allowedOptions, true)) { $mailerConfig[$name] = $value; } } } $config['components']['mailer'] = $mailerConfig; return $config; } public function restart() { parent::restart(); $this->resetApplication(); } /** * This functions closes the session of the application, if the application exists and has a session. * @internal */ public function closeSession() { if (isset(\Yii::$app) && \Yii::$app->has('session', true)) { \Yii::$app->session->close(); } } /** * Resets the applications' response object. * The method used depends on the module configuration. */ protected function resetResponse(Application $app) { $method = $this->responseCleanMethod; // First check the current response object. if (($app->response->hasEventHandlers(\yii\web\Response::EVENT_BEFORE_SEND) || $app->response->hasEventHandlers(\yii\web\Response::EVENT_AFTER_SEND) || $app->response->hasEventHandlers(\yii\web\Response::EVENT_AFTER_PREPARE) || count($app->response->getBehaviors()) > 0 ) && $method === self::CLEAN_RECREATE ) { Debug::debug(<<set('response', $app->getComponents()['response']); break; case self::CLEAN_CLEAR: $app->response->clear(); break; case self::CLEAN_MANUAL: break; } } protected function resetRequest(Application $app) { $method = $this->requestCleanMethod; $request = $app->request; // First check the current request object. if (count($request->getBehaviors()) > 0 && $method === self::CLEAN_RECREATE) { Debug::debug(<<set('request', $app->getComponents()['request']); break; case self::CLEAN_CLEAR: $request->getHeaders()->removeAll(); $request->setBaseUrl(null); $request->setHostInfo(null); $request->setPathInfo(null); $request->setScriptFile(null); $request->setScriptUrl(null); $request->setUrl(null); $request->setPort(null); $request->setSecurePort(null); $request->setAcceptableContentTypes(null); $request->setAcceptableLanguages(null); break; case self::CLEAN_MANUAL: break; } } /** * Called before each request, preparation happens here. */ protected function beforeRequest() { if ($this->recreateApplication) { $this->resetApplication(); return; } $application = $this->getApplication(); $this->resetResponse($application); $this->resetRequest($application); $definitions = $application->getComponents(true); foreach ($this->recreateComponents as $component) { // Only recreate if it has actually been instantiated. if ($application->has($component, true)) { $application->set($component, $definitions[$component]); } } } } response = $response; } public function getResponse() { if ($this->response === null) { throw new \LogicException('Response wasn\'t emitted yet'); } return $this->response; } public function clearResponse() { $this->response = null; } } getContent(); if ($content !== null) { fwrite($inputStream, $content); rewind($inputStream); } $queryParams = []; $postParams = []; $queryString = parse_url($request->getUri(), PHP_URL_QUERY); if ($queryString != '') { parse_str($queryString, $queryParams); } if ($request->getMethod() !== 'GET') { $postParams = $request->getParameters(); } $serverParams = $request->getServer(); if (!isset($serverParams['SCRIPT_NAME'])) { //required by WhoopsErrorHandler $serverParams['SCRIPT_NAME'] = 'Codeception'; } $cookies = $request->getCookies(); $headers = $this->extractHeaders($request); //set cookie header because dflydev/fig-cookies reads cookies from header if (!empty($cookies)) { $headers['cookie'] = implode(';', array_map(function ($key, $value) { return "$key=$value"; }, array_keys($cookies), $cookies)); } $zendRequest = new ServerRequest( $serverParams, $this->convertFiles($request->getFiles()), $request->getUri(), $request->getMethod(), $inputStream, $headers, $cookies, $queryParams, $postParams ); $this->request = $zendRequest; $cwd = getcwd(); chdir(codecept_root_dir()); if ($this->config['recreateApplicationBetweenRequests'] === true || $this->application === null) { $application = $this->initApplication(); } else { $application = $this->application; } if (method_exists($application, 'handle')) { //Zend Expressive v3 $response = $application->handle($zendRequest); } else { //Older versions $application->run($zendRequest); $response = $this->responseCollector->getResponse(); $this->responseCollector->clearResponse(); } chdir($cwd); return new Response( (string)$response->getBody(), $response->getStatusCode(), $response->getHeaders() ); } private function convertFiles(array $files) { $fileObjects = []; foreach ($files as $fieldName => $file) { if ($file instanceof UploadedFile) { $fileObjects[$fieldName] = $file; } elseif (!isset($file['tmp_name']) && !isset($file['name'])) { $fileObjects[$fieldName] = $this->convertFiles($file); } else { $fileObjects[$fieldName] = new UploadedFile( $file['tmp_name'], $file['size'], $file['error'], $file['name'], $file['type'] ); } } return $fileObjects; } private function extractHeaders(BrowserKitRequest $request) { $headers = []; $server = $request->getServer(); $contentHeaders = ['Content-Length' => true, 'Content-Md5' => true, 'Content-Type' => true]; foreach ($server as $header => $val) { $header = html_entity_decode(implode('-', array_map('ucfirst', explode('-', strtolower(str_replace('_', '-', $header))))), ENT_NOQUOTES); if (strpos($header, 'Http-') === 0) { $headers[substr($header, 5)] = $val; } elseif (isset($contentHeaders[$header])) { $headers[$header] = $val; } } return $headers; } public function initApplication() { $cwd = getcwd(); $projectDir = Configuration::projectDir(); chdir($projectDir); $this->container = require $projectDir . $this->config['container']; $app = $this->container->get(\Zend\Expressive\Application::class); $middlewareFactory = null; if ($this->container->has(\Zend\Expressive\MiddlewareFactory::class)) { $middlewareFactory = $this->container->get(\Zend\Expressive\MiddlewareFactory::class); } $pipelineFile = $projectDir . 'config/pipeline.php'; if (file_exists($pipelineFile)) { $pipelineFunction = require $pipelineFile; if (is_callable($pipelineFunction) && $middlewareFactory) { $pipelineFunction($app, $middlewareFactory, $this->container); } } $routesFile = $projectDir . 'config/routes.php'; if (file_exists($routesFile)) { $routesFunction = require $routesFile; if (is_callable($routesFunction) && $middlewareFactory) { $routesFunction($app, $middlewareFactory, $this->container); } } chdir($cwd); $this->application = $app; $this->initResponseCollector(); return $app; } private function initResponseCollector() { if (!method_exists($this->application, 'getEmitter')) { //Does not exist in Zend Expressive v3 return; } /** * @var Zend\Expressive\Emitter\EmitterStack */ $emitterStack = $this->application->getEmitter(); while (!$emitterStack->isEmpty()) { $emitterStack->pop(); } $this->responseCollector = new ResponseCollector; $emitterStack->unshift($this->responseCollector); } public function getContainer() { return $this->container; } /** * @param Application */ public function setApplication(Application $application) { $this->application = $application; $this->initResponseCollector(); } public function setConfig(array $config) { $this->config = $config; } } serviceManager = $serviceManager; } public function get($name, $usePeeringServiceManagers = true) { if (parent::has($name)) { return parent::get($name, $usePeeringServiceManagers); } return $this->serviceManager->get($name); } public function has($name, $checkAbstractFactories = true, $usePeeringServiceManagers = true) { if (parent::has($name, $checkAbstractFactories, $usePeeringServiceManagers)) { return true; } if (preg_match('/doctrine/i', $name)) { return $this->serviceManager->has($name); } return false; } public function setService($name, $service) { parent::setService($name, $service); } } applicationConfig = $applicationConfig; $this->createApplication(); } /** * @param Request $request * * @return Response * @throws \Exception */ public function doRequest($request) { $this->createApplication(); $zendRequest = $this->application->getRequest(); $uri = new HttpUri($request->getUri()); $queryString = $uri->getQuery(); $method = strtoupper($request->getMethod()); $zendRequest->setCookies(new Parameters($request->getCookies())); $query = []; $post = []; $content = $request->getContent(); if ($queryString) { parse_str($queryString, $query); } if ($method !== HttpRequest::METHOD_GET) { $post = $request->getParameters(); } $zendRequest->setQuery(new Parameters($query)); $zendRequest->setPost(new Parameters($post)); $zendRequest->setFiles(new Parameters($request->getFiles())); $zendRequest->setContent($content); $zendRequest->setMethod($method); $zendRequest->setUri($uri); $requestUri = $uri->getPath(); if (!empty($queryString)) { $requestUri .= '?' . $queryString; } $zendRequest->setRequestUri($requestUri); $zendRequest->setHeaders($this->extractHeaders($request)); $this->application->run(); // get the response *after* the application has run, because other ZF // libraries like API Agility may *replace* the application's response // $zendResponse = $this->application->getResponse(); $this->zendRequest = $zendRequest; $exception = $this->application->getMvcEvent()->getParam('exception'); if ($exception instanceof \Exception) { throw $exception; } $response = new Response( $zendResponse->getBody(), $zendResponse->getStatusCode(), $zendResponse->getHeaders()->toArray() ); return $response; } /** * @return \Zend\Http\PhpEnvironment\Request */ public function getZendRequest() { return $this->zendRequest; } private function extractHeaders(BrowserKitRequest $request) { $headers = []; $server = $request->getServer(); $contentHeaders = ['Content-Length' => true, 'Content-Md5' => true, 'Content-Type' => true]; foreach ($server as $header => $val) { $header = html_entity_decode(implode('-', array_map('ucfirst', explode('-', strtolower(str_replace('_', '-', $header))))), ENT_NOQUOTES); if (strpos($header, 'Http-') === 0) { $headers[substr($header, 5)] = $val; } elseif (isset($contentHeaders[$header])) { $headers[$header] = $val; } } $zendHeaders = new HttpHeaders(); $zendHeaders->addHeaders($headers); return $zendHeaders; } public function grabServiceFromContainer($service) { $serviceManager = $this->application->getServiceManager(); if (!$serviceManager->has($service)) { throw new \PHPUnit\Framework\AssertionFailedError("Service $service is not available in container"); } if ($service === 'Doctrine\ORM\EntityManager' && !isset($this->persistentServiceManager)) { if (!method_exists($serviceManager, 'addPeeringServiceManager')) { throw new ModuleException('Codeception\Module\ZF2', 'integration with Doctrine2 module is not compatible with ZF3'); } $this->persistentServiceManager = new PersistentServiceManager($serviceManager); } return $serviceManager->get($service); } public function addServiceToContainer($name, $service) { if (!isset($this->persistentServiceManager)) { $serviceManager = $this->application->getServiceManager(); if (!method_exists($serviceManager, 'addPeeringServiceManager')) { throw new ModuleException('Codeception\Module\ZF2', 'addServiceToContainer method is not compatible with ZF3'); } $this->persistentServiceManager = new PersistentServiceManager($serviceManager); $serviceManager->addPeeringServiceManager($this->persistentServiceManager); $serviceManager->setRetrieveFromPeeringManagerFirst(true); } $this->persistentServiceManager->setAllowOverride(true); $this->persistentServiceManager->setService($name, $service); $this->persistentServiceManager->setAllowOverride(false); } private function createApplication() { $this->application = Application::init($this->applicationConfig); $serviceManager = $this->application->getServiceManager(); if (isset($this->persistentServiceManager)) { $serviceManager->addPeeringServiceManager($this->persistentServiceManager); $serviceManager->setRetrieveFromPeeringManagerFirst(true); } $sendResponseListener = $serviceManager->get('SendResponseListener'); $events = $this->application->getEventManager(); if (class_exists('Zend\EventManager\StaticEventManager')) { $events->detach($sendResponseListener); //ZF2 } else { $events->detach([$sendResponseListener, 'sendResponse']); //ZF3 } } } $line"; break; case '-': $line = "$line"; break; } $colorizedMessage .= $line . "\n"; } return trim($colorizedMessage); } } getDiff($failure->getExpectedAsString(), $failure->getActualAsString()); if (!$diff) { return null; } return $diff; } /** * @param string $expected * @param string $actual * @return string */ private function getDiff($expected = '', $actual = '') { if (!$actual && !$expected) { return ''; } $differ = new Differ(''); return $differ->diff($expected, $actual); } } message = $message; $this->output = $output; } public function with($param) { $args = array_merge([$this->message], func_get_args()); $this->message = call_user_func_array('sprintf', $args); return $this; } public function style($name) { $this->message = sprintf('<%s>%s', $name, $this->message, $name); return $this; } public function width($length, $char = ' ') { $message_length = $this->getLength(); if ($message_length < $length) { $this->message .= str_repeat($char, $length - $message_length); } return $this; } public function cut($length) { $this->message = mb_substr($this->message, 0, $length, 'utf-8'); return $this; } public function write($verbose = OutputInterface::VERBOSITY_NORMAL) { if ($verbose > $this->output->getVerbosity()) { return; } $this->output->write($this->message); } public function writeln($verbose = OutputInterface::VERBOSITY_NORMAL) { if ($verbose > $this->output->getVerbosity()) { return; } $this->output->writeln($this->message); } public function prepend($string) { if ($string instanceof Message) { $string = $string->getMessage(); } $this->message = $string . $this->message; return $this; } public function append($string) { if ($string instanceof Message) { $string = $string->getMessage(); } $this->message .= $string; return $this; } public function apply($func) { $this->message = call_user_func($func, $this->message); return $this; } public function center($char) { $this->message = $char . $this->message . $char; return $this; } /** * @return mixed */ public function getMessage() { return $this->message; } public function block($style) { $this->message = $this->output->formatHelper->formatBlock($this->message, $style, true); return $this; } public function getLength($includeTags = false) { return mb_strwidth($includeTags ? $this->message : strip_tags($this->message), 'utf-8'); } public static function ucfirst($text) { return mb_strtoupper(mb_substr($text, 0, 1, 'utf-8'), 'utf-8') . mb_substr($text, 1, null, 'utf-8'); } public function __toString() { return $this->message; } } output = $output; $this->diffFactory = new DiffFactory(); $this->colorizer = new Colorizer(); } /** * @param string $text * @return Message */ public function message($text = '') { return new Message($text, $this->output); } /** * @param ComparisonFailure $failure * @return string */ public function prepareComparisonFailureMessage(ComparisonFailure $failure) { $diff = $this->diffFactory->createDiff($failure); if (!$diff) { return ''; } $diff = $this->colorizer->colorize($diff); return "\n- Expected | + Actual\n$diff"; } } true, 'verbosity' => self::VERBOSITY_NORMAL, 'interactive' => true ]; /** * @var \Symfony\Component\Console\Helper\FormatterHelper */ public $formatHelper; public $waitForDebugOutput = true; protected $isInteractive = false; public function __construct($config) { $this->config = array_merge($this->config, $config); // enable interactive output mode for CLI $this->isInteractive = $this->config['interactive'] && isset($_SERVER['TERM']) && php_sapi_name() == 'cli' && $_SERVER['TERM'] != 'linux'; $formatter = new OutputFormatter($this->config['colors']); $formatter->setStyle('default', new OutputFormatterStyle()); $formatter->setStyle('bold', new OutputFormatterStyle(null, null, ['bold'])); $formatter->setStyle('focus', new OutputFormatterStyle('magenta', null, ['bold'])); $formatter->setStyle('ok', new OutputFormatterStyle('green', null, ['bold'])); $formatter->setStyle('error', new OutputFormatterStyle('white', 'red', ['bold'])); $formatter->setStyle('fail', new OutputFormatterStyle('red', null, ['bold'])); $formatter->setStyle('pending', new OutputFormatterStyle('yellow', null, ['bold'])); $formatter->setStyle('debug', new OutputFormatterStyle('cyan')); $formatter->setStyle('comment', new OutputFormatterStyle('yellow')); $formatter->setStyle('info', new OutputFormatterStyle('green')); $this->formatHelper = new FormatterHelper(); parent::__construct($this->config['verbosity'], $this->config['colors'], $formatter); } public function isInteractive() { return $this->isInteractive; } protected function clean($message) { // clear json serialization $message = str_replace('\/', '/', $message); return $message; } public function debug($message) { $message = print_r($message, true); $message = str_replace("\n", "\n ", $message); $message = $this->clean($message); $message = OutputFormatter::escape($message); if ($this->waitForDebugOutput) { $this->writeln(''); $this->waitForDebugOutput = false; } $this->writeln(" $message"); } public function message($message) { $message = call_user_func_array('sprintf', func_get_args()); return new Message($message, $this); } public function exception(\Exception $e) { $class = get_class($e); $this->writeln(""); $this->writeln("(![ $class ]!)"); $this->writeln($e->getMessage()); $this->writeln(""); } public function notification($message) { $this->writeln("$message"); } } config = $config; //Convert To Array Format if (isset($this->config['dump']) && !is_array($this->config['dump'])) { $this->config['dump'] = [$this->config['dump']]; } } /** * Builds out a command replacing any found `$key` with its value if found in the given configuration. * * Process any $key found in the configuration array as a key of the array and replaces it with * the found value for the key. Example: * * ```php * 'Mauro']; * * // With the above parameters it will return `'Hello Mauro'`. * ``` * * @param string $command The command to be evaluated using the given config * @param string|null $dumpFile The dump file to build the command with. * @return string The resulting command string after evaluating any configuration's key */ protected function buildCommand($command, $dumpFile = null) { $dsn = isset($this->config['dsn']) ? $this->config['dsn'] : ''; $dsnVars = []; $dsnWithoutDriver = preg_replace('/^[a-z]+:/i', '', $dsn); foreach (explode(';', $dsnWithoutDriver) as $item) { $keyValueTuple = explode('=', $item); if (count($keyValueTuple) > 1) { list($k, $v) = array_values($keyValueTuple); $dsnVars[$k] = $v; } } $vars = array_merge($dsnVars, $this->config); if ($dumpFile !== null) { $vars['dump'] = $dumpFile; } foreach ($vars as $key => $value) { if (!is_array($value)) { $vars['$'.$key] = $value; } unset($vars[$key]); } return str_replace(array_keys($vars), array_values($vars), $command); } /** * Executes the command built using the Db module configuration. * * Uses the PHP `exec` to spin off a child process for the built command. * * @return bool */ public function run() { foreach ($this->buildCommands() as $command) { $this->runCommand($command); } return true; } private function runCommand($command) { codecept_debug("[Db] Executing Populator: `$command`"); exec($command, $output, $exitCode); if (0 !== $exitCode) { throw new \RuntimeException( "The populator command did not end successfully: \n" . " Exit code: $exitCode \n" . " Output:" . implode("\n", $output) ); } codecept_debug("[Db] Populator Finished."); } public function buildCommands() { if ($this->commands !== null) { return $this->commands; } elseif (!isset($this->config['dump']) || $this->config['dump'] === false) { return [$this->buildCommand($this->config['populator'])]; } $this->commands = []; foreach ($this->config['dump'] as $dumpFile) { $this->commands[] = $this->buildCommand($this->config['populator'], $dumpFile); } return $this->commands; } } fallback = $fallback; } public function get($className) { // normalize namespace $className = ltrim($className, '\\'); return isset($this->container[$className]) ? $this->container[$className] : null; } public function set($class) { $this->container[get_class($class)] = $class; } /** * @param string $className * @param array $constructorArgs * @param string $injectMethodName Method which will be invoked after object creation; * Resolved dependencies will be passed to it as arguments * @throws InjectionException * @return null|object */ public function instantiate( $className, $constructorArgs = null, $injectMethodName = self::DEFAULT_INJECT_METHOD_NAME ) { // normalize namespace $className = ltrim($className, '\\'); // get class from container if (isset($this->container[$className])) { if ($this->container[$className] instanceof $className) { return $this->container[$className]; } throw new InjectionException("Failed to resolve cyclic dependencies for class '$className'"); } // get class from parent container if ($this->fallback) { if ($class = $this->fallback->get($className)) { return $class; } } $this->container[$className] = false; // flag that object is being instantiated $reflectedClass = new \ReflectionClass($className); if (!$reflectedClass->isInstantiable()) { return null; } $reflectedConstructor = $reflectedClass->getConstructor(); if (is_null($reflectedConstructor)) { $object = new $className; } else { try { if (!$constructorArgs) { $constructorArgs = $this->prepareArgs($reflectedConstructor); } } catch (\Exception $e) { throw new InjectionException("Failed to create instance of '$className'. " . $e->getMessage()); } $object = $reflectedClass->newInstanceArgs($constructorArgs); } if ($injectMethodName) { $this->injectDependencies($object, $injectMethodName); } $this->container[$className] = $object; return $object; } /** * @param $object * @param string $injectMethodName Method which will be invoked with resolved dependencies as its arguments * @throws InjectionException */ public function injectDependencies($object, $injectMethodName = self::DEFAULT_INJECT_METHOD_NAME, $defaults = []) { if (!is_object($object)) { return; } $reflectedObject = new \ReflectionObject($object); if (!$reflectedObject->hasMethod($injectMethodName)) { return; } $reflectedMethod = $reflectedObject->getMethod($injectMethodName); try { $args = $this->prepareArgs($reflectedMethod, $defaults); } catch (\Exception $e) { $msg = $e->getMessage(); if ($e->getPrevious()) { // injection failed because PHP code is invalid. See #3869 $msg .= '; '. $e->getPrevious(); } throw new InjectionException( "Failed to inject dependencies in instance of '{$reflectedObject->name}'. $msg" ); } if (!$reflectedMethod->isPublic()) { $reflectedMethod->setAccessible(true); } $reflectedMethod->invokeArgs($object, $args); } /** * @param \ReflectionMethod $method * @param $defaults * @throws InjectionException * @return array */ protected function prepareArgs(\ReflectionMethod $method, $defaults = []) { $args = []; $parameters = $method->getParameters(); foreach ($parameters as $k => $parameter) { $dependency = $parameter->getClass(); if (is_null($dependency)) { if (!$parameter->isOptional()) { if (!isset($defaults[$k])) { throw new InjectionException("Parameter '$parameter->name' must have default value."); } $args[] = $defaults[$k]; continue; } $args[] = $parameter->getDefaultValue(); } else { $arg = $this->instantiate($dependency->name); if (is_null($arg)) { throw new InjectionException("Failed to resolve dependency '{$dependency->name}'."); } $args[] = $arg; } } return $args; } } $config['region'], ]; if (! empty($config['key']) && ! empty($config['secret'])) { $params['credentials'] = new Credentials($config['key'], $config['secret']); } if (! empty($config['profile'])) { $params['profile'] = $config['profile']; } if (! empty($config['version'])) { $params['version'] = $config['version']; } if (! empty($config['endpoint'])) { $params['endpoint'] = $config['endpoint']; } $this->queue = new SqsClient($params); if (!$this->queue) { throw new TestRuntimeException('connection failed or timed-out.'); } } /** * Post/Put a message on to the queue server * * @param string $message Message Body to be send * @param string $queue Queue Name */ public function addMessageToQueue($message, $queue) { $this->queue->sendMessage([ 'QueueUrl' => $this->getQueueURL($queue), 'MessageBody' => $message, ]); } /** * Return a list of queues/tubes on the queueing server * * @return array Array of Queues */ public function getQueues() { $queueNames = []; $queues = $this->queue->listQueues(['QueueNamePrefix' => ''])->get('QueueUrls'); foreach ($queues as $queue) { $tokens = explode('/', $queue); $queueNames[] = $tokens[sizeof($tokens) - 1]; } return $queueNames; } /** * Count the current number of messages on the queue. * * @param $queue Queue Name * * @return int Count */ public function getMessagesCurrentCountOnQueue($queue) { return $this->queue->getQueueAttributes([ 'QueueUrl' => $this->getQueueURL($queue), 'AttributeNames' => ['ApproximateNumberOfMessages'], ])->get('Attributes')['ApproximateNumberOfMessages']; } /** * Count the total number of messages on the queue. * * @param $queue Queue Name * * @return int Count */ public function getMessagesTotalCountOnQueue($queue) { return $this->queue->getQueueAttributes([ 'QueueUrl' => $this->getQueueURL($queue), 'AttributeNames' => ['ApproximateNumberOfMessages'], ])->get('Attributes')['ApproximateNumberOfMessages']; } public function clearQueue($queue) { $queueURL = $this->getQueueURL($queue); while (true) { $res = $this->queue->receiveMessage(['QueueUrl' => $queueURL]); if (!$res->getPath('Messages')) { return; } foreach ($res->getPath('Messages') as $msg) { $this->queue->deleteMessage([ 'QueueUrl' => $queueURL, 'ReceiptHandle' => $msg['ReceiptHandle'] ]); } } } /** * Get the queue/tube URL from the queue name (AWS function only) * * @param $queue Queue Name * * @return string Queue URL */ private function getQueueURL($queue) { $queues = $this->queue->listQueues(['QueueNamePrefix' => ''])->get('QueueUrls'); foreach ($queues as $queueURL) { $tokens = explode('/', $queueURL); if (strtolower($queue) == strtolower($tokens[sizeof($tokens) - 1])) { return $queueURL; } } throw new TestRuntimeException('queue [' . $queue . '] not found'); } public function getRequiredConfig() { return ['region']; } public function getDefaultConfig() { return []; } } queue = new Pheanstalk($config['host'], $config['port'], $config['timeout']); } /** * Post/Put a message on to the queue server * * @param string $message Message Body to be send * @param string $queue Queue Name */ public function addMessageToQueue($message, $queue) { $this->queue->putInTube($queue, $message); } /** * Count the total number of messages on the queue. * * @param $queue Queue Name * * @return int Count */ public function getMessagesTotalCountOnQueue($queue) { try { return $this->queue->statsTube($queue)['total-jobs']; } catch (ConnectionException $ex) { \PHPUnit\Framework\Assert::fail("queue [$queue] not found"); } } public function clearQueue($queue = 'default') { while ($job = $this->queue->reserveFromTube($queue, 0)) { $this->queue->delete($job); } } /** * Return a list of queues/tubes on the queueing server * * @return array Array of Queues */ public function getQueues() { return $this->queue->listTubes(); } /** * Count the current number of messages on the queue. * * @param $queue Queue Name * * @return int Count */ public function getMessagesCurrentCountOnQueue($queue) { try { return $this->queue->statsTube($queue)['current-jobs-ready']; } catch (ConnectionException $e) { \PHPUnit\Framework\Assert::fail("queue [$queue] not found"); } } public function getRequiredConfig() { return ['host']; } public function getDefaultConfig() { return ['port' => 11300, 'timeout' => 90]; } } primary-key * * @var array */ protected $primaryKeys = []; public static function connect($dsn, $user, $password, $options = null) { $dbh = new \PDO($dsn, $user, $password, $options); $dbh->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION); return $dbh; } /** * @static * * @param $dsn * @param $user * @param $password * @param [optional] $options * * @see http://php.net/manual/en/pdo.construct.php * @see http://php.net/manual/de/ref.pdo-mysql.php#pdo-mysql.constants * * @return Db|SqlSrv|MySql|Oci|PostgreSql|Sqlite */ public static function create($dsn, $user, $password, $options = null) { $provider = self::getProvider($dsn); switch ($provider) { case 'sqlite': return new Sqlite($dsn, $user, $password, $options); case 'mysql': return new MySql($dsn, $user, $password, $options); case 'pgsql': return new PostgreSql($dsn, $user, $password, $options); case 'mssql': case 'dblib': case 'sqlsrv': return new SqlSrv($dsn, $user, $password, $options); case 'oci': return new Oci($dsn, $user, $password, $options); default: return new Db($dsn, $user, $password, $options); } } public static function getProvider($dsn) { return substr($dsn, 0, strpos($dsn, ':')); } /** * @param $dsn * @param $user * @param $password * @param [optional] $options * * @see http://php.net/manual/en/pdo.construct.php * @see http://php.net/manual/de/ref.pdo-mysql.php#pdo-mysql.constants */ public function __construct($dsn, $user, $password, $options = null) { $this->dbh = new \PDO($dsn, $user, $password, $options); $this->dbh->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION); $this->dsn = $dsn; $this->user = $user; $this->password = $password; $this->options = $options; } public function __destruct() { if ($this->dbh->inTransaction()) { $this->dbh->rollBack(); } $this->dbh = null; } public function getDbh() { return $this->dbh; } public function getDb() { $matches = []; $matched = preg_match('~dbname=(\w+)~s', $this->dsn, $matches); if (!$matched) { return false; } return $matches[1]; } public function cleanup() { } /** * Set the lock waiting interval for the database session * @param int $seconds * @return void */ public function setWaitLock($seconds) { } public function load($sql) { $query = ''; $delimiter = ';'; $delimiterLength = 1; foreach ($sql as $sqlLine) { if (preg_match('/DELIMITER ([\;\$\|\\\\]+)/i', $sqlLine, $match)) { $delimiter = $match[1]; $delimiterLength = strlen($delimiter); continue; } $parsed = $this->sqlLine($sqlLine); if ($parsed) { continue; } $query .= "\n" . rtrim($sqlLine); if (substr($query, -1 * $delimiterLength, $delimiterLength) == $delimiter) { $this->sqlQuery(substr($query, 0, -1 * $delimiterLength)); $query = ''; } } if ($query !== '') { $this->sqlQuery($query); } } public function insert($tableName, array &$data) { $columns = array_map( [$this, 'getQuotedName'], array_keys($data) ); return sprintf( "INSERT INTO %s (%s) VALUES (%s)", $this->getQuotedName($tableName), implode(', ', $columns), implode(', ', array_fill(0, count($data), '?')) ); } public function select($column, $table, array &$criteria) { $where = $this->generateWhereClause($criteria); $query = "SELECT %s FROM %s %s"; return sprintf($query, $column, $this->getQuotedName($table), $where); } private function getSupportedOperators() { return [ 'like', '!=', '<=', '>=', '<', '>', ]; } protected function generateWhereClause(array &$criteria) { if (empty($criteria)) { return ''; } $operands = $this->getSupportedOperators(); $params = []; foreach ($criteria as $k => $v) { if ($v === null) { $params[] = $this->getQuotedName($k) . " IS NULL "; unset($criteria[$k]); continue; } $hasOperand = false; // search for equals - no additional operand given foreach ($operands as $operand) { if (!stripos($k, " $operand") > 0) { continue; } $hasOperand = true; $k = str_ireplace(" $operand", '', $k); $operand = strtoupper($operand); $params[] = $this->getQuotedName($k) . " $operand ? "; break; } if (!$hasOperand) { $params[] = $this->getQuotedName($k) . " = ? "; } } return 'WHERE ' . implode('AND ', $params); } public function deleteQueryByCriteria($table, array $criteria) { $where = $this->generateWhereClause($criteria); $query = 'DELETE FROM ' . $this->getQuotedName($table) . ' ' . $where; $this->executeQuery($query, array_values($criteria)); } public function lastInsertId($table) { return $this->getDbh()->lastInsertId(); } public function getQuotedName($name) { return '"' . str_replace('.', '"."', $name) . '"'; } protected function sqlLine($sql) { $sql = trim($sql); return ( $sql === '' || $sql === ';' || preg_match('~^((--.*?)|(#))~s', $sql) ); } protected function sqlQuery($query) { try { $this->dbh->exec($query); } catch (\PDOException $e) { throw new ModuleException( 'Codeception\Module\Db', $e->getMessage() . "\nSQL query being executed: " . $query ); } } public function executeQuery($query, array $params) { $sth = $this->dbh->prepare($query); if (!$sth) { throw new \Exception("Query '$query' can't be prepared."); } $i = 0; foreach ($params as $value) { $i++; if (is_bool($value)) { $type = \PDO::PARAM_BOOL; } elseif (is_int($value)) { $type = \PDO::PARAM_INT; } else { $type = \PDO::PARAM_STR; } $sth->bindValue($i, $value, $type); } $sth->execute(); return $sth; } /** * @param string $tableName * * @return array[string] */ public function getPrimaryKey($tableName) { return []; } /** * @return bool */ protected function flushPrimaryColumnCache() { $this->primaryKeys = []; return empty($this->primaryKeys); } public function update($table, array $data, array $criteria) { if (empty($data)) { throw new \InvalidArgumentException( "Query update can't be prepared without data." ); } $set = []; foreach ($data as $column => $value) { $set[] = $this->getQuotedName($column) . " = ?"; } $where = $this->generateWhereClause($criteria); return sprintf('UPDATE %s SET %s %s', $this->getQuotedName($table), implode(', ', $set), $where); } public function getOptions() { return $this->options; } } queue = new \IronMQ([ "token" => $config['token'], "project_id" => $config['project'], "host" => $config['host'] ]); if (!$this->queue) { \PHPUnit\Framework\Assert::fail('connection failed or timed-out.'); } } /** * Post/Put a message on to the queue server * * @param string $message Message Body to be send * @param string $queue Queue Name */ public function addMessageToQueue($message, $queue) { $this->queue->postMessage($queue, $message); } /** * Return a list of queues/tubes on the queueing server * * @return array Array of Queues */ public function getQueues() { // Format the output to suit $queues = []; foreach ($this->queue->getQueues() as $queue) { $queues[] = $queue->name; } return $queues; } /** * Count the current number of messages on the queue. * * @param $queue Queue Name * * @return int Count */ public function getMessagesCurrentCountOnQueue($queue) { try { return $this->queue->getQueue($queue)->size; } catch (\Http_Exception $ex) { \PHPUnit\Framework\Assert::fail("queue [$queue] not found"); } } /** * Count the total number of messages on the queue. * * @param $queue Queue Name * * @return int Count */ public function getMessagesTotalCountOnQueue($queue) { try { return $this->queue->getQueue($queue)->total_messages; } catch (\Http_Exception $e) { \PHPUnit\Framework\Assert::fail("queue [$queue] not found"); } } public function clearQueue($queue) { try { $this->queue->clearQueue($queue); } catch (\Http_Exception $ex) { \PHPUnit\Framework\Assert::fail("queue [$queue] not found"); } } public function getRequiredConfig() { return ['host', 'token', 'project']; } public function getDefaultConfig() { return []; } } client = new \MongoDB\Client($dsn, $options); $this->dbh = $this->client->selectDatabase($this->dbName); } catch (\MongoDB\Driver\Exception $e) { throw new ModuleException($this, sprintf('Failed to open Mongo connection: %s', $e->getMessage())); } } /** * Connect to the Mongo server using the legacy mongo extension. */ protected function setupMongo($dsn, $options) { try { $this->client = new \MongoClient($dsn, $options); $this->dbh = $this->client->selectDB($this->dbName); } catch (\MongoConnectionException $e) { throw new ModuleException($this, sprintf('Failed to open Mongo connection: %s', $e->getMessage())); } } /** * Clean up the Mongo database using the MongoDB extension. */ protected function cleanupMongoDB() { try { $this->dbh->drop(); } catch (\MongoDB\Driver\Exception $e) { throw new \Exception(sprintf('Failed to drop the DB: %s', $e->getMessage())); } } /** * Clean up the Mongo database using the legacy Mongo extension. */ protected function cleanupMongo() { try { $list = $this->dbh->listCollections(); } catch (\MongoException $e) { throw new \Exception(sprintf('Failed to list collections of the DB: %s', $e->getMessage())); } foreach ($list as $collection) { try { $collection->drop(); } catch (\MongoException $e) { throw new \Exception(sprintf('Failed to drop collection: %s', $e->getMessage())); } } } /** * $dsn has to contain db_name after the host. E.g. "mongodb://localhost:27017/mongo_test_db" * * @static * * @param $dsn * @param $user * @param $password * * @throws ModuleConfigException * @throws \Exception */ public function __construct($dsn, $user, $password) { $this->legacy = extension_loaded('mongodb') === false && class_exists('\\MongoClient') && strpos(\MongoClient::VERSION, 'mongofill') === false; /* defining DB name */ $this->dbName = preg_replace('/\?.*/', '', substr($dsn, strrpos($dsn, '/') + 1)); if (strlen($this->dbName) == 0) { throw new ModuleConfigException($this, 'Please specify valid $dsn with DB name after the host:port'); } /* defining host */ if (strpos($dsn, 'mongodb://') !== false) { $this->host = str_replace('mongodb://', '', preg_replace('/\?.*/', '', $dsn)); } else { $this->host = $dsn; } $this->host = rtrim(str_replace($this->dbName, '', $this->host), '/'); $options = [ 'connect' => true ]; if ($user && $password) { $options += [ 'username' => $user, 'password' => $password ]; } $this->{$this->legacy ? 'setupMongo' : 'setupMongoDB'}($dsn, $options); $this->dsn = $dsn; $this->user = $user; $this->password = $password; } /** * @static * * @param $dsn * @param $user * @param $password * * @return MongoDb */ public static function create($dsn, $user, $password) { return new MongoDb($dsn, $user, $password); } public function cleanup() { $this->{$this->legacy ? 'cleanupMongo' : 'cleanupMongoDB'}(); } /** * dump file has to be a javascript document where one can use all the mongo shell's commands * just FYI: this file can be easily created be RockMongo's export button * * @param $dumpFile */ public function load($dumpFile) { $cmd = sprintf( 'mongo %s %s%s', $this->host . '/' . $this->dbName, $this->createUserPasswordCmdString(), escapeshellarg($dumpFile) ); shell_exec($cmd); } public function loadFromMongoDump($dumpFile) { list($host, $port) = $this->getHostPort(); $cmd = sprintf( "mongorestore %s --host %s --port %s -d %s %s %s", $this->quiet, $host, $port, $this->dbName, $this->createUserPasswordCmdString(), escapeshellarg($dumpFile) ); shell_exec($cmd); } public function loadFromTarGzMongoDump($dumpFile) { list($host, $port) = $this->getHostPort(); $getDirCmd = sprintf( "tar -tf %s | awk 'BEGIN { FS = \"/\" } ; { print $1 }' | uniq", escapeshellarg($dumpFile) ); $dirCountCmd = $getDirCmd . ' | wc -l'; if (trim(shell_exec($dirCountCmd)) !== '1') { throw new ModuleException( $this, 'Archive MUST contain single directory with db dump' ); } $dirName = trim(shell_exec($getDirCmd)); $cmd = sprintf( 'tar -xzf %s && mongorestore %s --host %s --port %s -d %s %s %s && rm -r %s', escapeshellarg($dumpFile), $this->quiet, $host, $port, $this->dbName, $this->createUserPasswordCmdString(), $dirName, $dirName ); shell_exec($cmd); } private function createUserPasswordCmdString() { if ($this->user && $this->password) { return sprintf( '--username %s --password %s ', $this->user, $this->password ); } return ''; } public function getDbh() { return $this->dbh; } public function setDatabase($dbName) { $this->dbh = $this->client->{$this->legacy ? 'selectDB' : 'selectDatabase'}($dbName); } /** * Determine if this driver is using the legacy extension or not. * * @return bool */ public function isLegacy() { return $this->legacy; } private function getHostPort() { $hostPort = explode(':', $this->host); if (count($hostPort) === 2) { return $hostPort; } if (count($hostPort) === 1) { return [$hostPort[0], self::DEFAULT_PORT]; } throw new ModuleException($this, '$dsn MUST be like (mongodb://):/'); } public function setQuiet($quiet) { $this->quiet = $quiet ? '--quiet' : ''; } } dbh->exec('SET FOREIGN_KEY_CHECKS=0;'); $res = $this->dbh->query("SHOW FULL TABLES WHERE TABLE_TYPE LIKE '%TABLE';")->fetchAll(); foreach ($res as $row) { $this->dbh->exec('drop table `' . $row[0] . '`'); } $this->dbh->exec('SET FOREIGN_KEY_CHECKS=1;'); } protected function sqlQuery($query) { $this->dbh->exec('SET FOREIGN_KEY_CHECKS=0;'); parent::sqlQuery($query); $this->dbh->exec('SET FOREIGN_KEY_CHECKS=1;'); } public function getQuotedName($name) { return '`' . str_replace('.', '`.`', $name) . '`'; } /** * @param string $tableName * * @return array[string] */ public function getPrimaryKey($tableName) { if (!isset($this->primaryKeys[$tableName])) { $primaryKey = []; $stmt = $this->getDbh()->query( 'SHOW KEYS FROM ' . $this->getQuotedName($tableName) . ' WHERE Key_name = "PRIMARY"' ); $columns = $stmt->fetchAll(\PDO::FETCH_ASSOC); foreach ($columns as $column) { $primaryKey []= $column['Column_name']; } $this->primaryKeys[$tableName] = $primaryKey; } return $this->primaryKeys[$tableName]; } } dbh->exec('ALTER SESSION SET ddl_lock_timeout = ' . (int) $seconds); } public function cleanup() { $this->dbh->exec( "BEGIN FOR i IN (SELECT trigger_name FROM user_triggers) LOOP EXECUTE IMMEDIATE('DROP TRIGGER ' || user || '.\"' || i.trigger_name || '\"'); END LOOP; END;" ); $this->dbh->exec( "BEGIN FOR i IN (SELECT table_name FROM user_tables) LOOP EXECUTE IMMEDIATE('DROP TABLE ' || user || '.\"' || i.table_name || '\" CASCADE CONSTRAINTS'); END LOOP; END;" ); $this->dbh->exec( "BEGIN FOR i IN (SELECT sequence_name FROM user_sequences) LOOP EXECUTE IMMEDIATE('DROP SEQUENCE ' || user || '.\"' || i.sequence_name || '\"'); END LOOP; END;" ); $this->dbh->exec( "BEGIN FOR i IN (SELECT view_name FROM user_views) LOOP EXECUTE IMMEDIATE('DROP VIEW ' || user || '.\"' || i.view_name || '\"'); END LOOP; END;" ); } /** * SQL commands should ends with `//` in the dump file * IF you want to load triggers too. * IF you do not want to load triggers you can use the `;` characters * but in this case you need to change the $delimiter from `//` to `;` * * @param $sql */ public function load($sql) { $query = ''; $delimiter = '//'; $delimiterLength = 2; foreach ($sql as $sqlLine) { if (preg_match('/DELIMITER ([\;\$\|\\\\]+)/i', $sqlLine, $match)) { $delimiter = $match[1]; $delimiterLength = strlen($delimiter); continue; } $parsed = $this->sqlLine($sqlLine); if ($parsed) { continue; } $query .= "\n" . rtrim($sqlLine); if (substr($query, -1 * $delimiterLength, $delimiterLength) == $delimiter) { $this->sqlQuery(substr($query, 0, -1 * $delimiterLength)); $query = ""; } } if ($query !== '') { $this->sqlQuery($query); } } /** * @param string $tableName * * @return array[string] */ public function getPrimaryKey($tableName) { if (!isset($this->primaryKeys[$tableName])) { $primaryKey = []; $query = "SELECT cols.column_name FROM all_constraints cons, all_cons_columns cols WHERE cols.table_name = ? AND cons.constraint_type = 'P' AND cons.constraint_name = cols.constraint_name AND cons.owner = cols.owner ORDER BY cols.table_name, cols.position"; $stmt = $this->executeQuery($query, [$tableName]); $columns = $stmt->fetchAll(\PDO::FETCH_ASSOC); foreach ($columns as $column) { $primaryKey []= $column['COLUMN_NAME']; } $this->primaryKeys[$tableName] = $primaryKey; } return $this->primaryKeys[$tableName]; } } sqlLine($sqlLine); if ($parsed) { continue; } // Ignore $$ inside SQL standard string syntax such as in INSERT statements. if (!preg_match('/\'.*\$\$.*\'/', $sqlLine)) { $pos = strpos($sqlLine, '$$'); if (($pos !== false) && ($pos >= 0)) { $dollarsOpen = !$dollarsOpen; } } if (preg_match('/SET search_path = .*/i', $sqlLine, $match)) { $this->searchPath = $match[0]; } $query .= "\n" . rtrim($sqlLine); if (!$dollarsOpen && substr($query, -1 * $delimiterLength, $delimiterLength) == $delimiter) { $this->sqlQuery(substr($query, 0, -1 * $delimiterLength)); $query = ''; } } if ($query !== '') { $this->sqlQuery($query); } } public function cleanup() { $this->dbh->exec('DROP SCHEMA IF EXISTS public CASCADE;'); $this->dbh->exec('CREATE SCHEMA public;'); } public function sqlLine($sql) { if (!$this->putline) { return parent::sqlLine($sql); } if ($sql == '\.') { $this->putline = false; pg_put_line($this->connection, $sql . "\n"); pg_end_copy($this->connection); pg_close($this->connection); } else { pg_put_line($this->connection, $sql . "\n"); } return true; } public function sqlQuery($query) { if (strpos(trim($query), 'COPY ') === 0) { if (!extension_loaded('pgsql')) { throw new ModuleException( '\Codeception\Module\Db', "To run 'COPY' commands 'pgsql' extension should be installed" ); } $constring = str_replace(';', ' ', substr($this->dsn, 6)); $constring .= ' user=' . $this->user; $constring .= ' password=' . $this->password; $this->connection = pg_connect($constring); if ($this->searchPath !== null) { pg_query($this->connection, $this->searchPath); } pg_query($this->connection, $query); $this->putline = true; } else { $this->dbh->exec($query); } } /** * Get the last inserted ID of table. */ public function lastInsertId($table) { /* * We make an assumption that the sequence name for this table * is based on how postgres names sequences for SERIAL columns */ $sequenceName = $this->getQuotedName($table . '_id_seq'); $lastSequence = null; try { $lastSequence = $this->getDbh()->lastInsertId($sequenceName); } catch (\PDOException $e) { // in this case, the sequence name might be combined with the primary key name } // here we check if for instance, it's something like table_primary_key_seq instead of table_id_seq // this could occur when you use some kind of import tool like pgloader if (!$lastSequence) { $primaryKeys = $this->getPrimaryKey($table); $pkName = array_shift($primaryKeys); $lastSequence = $this->getDbh()->lastInsertId($this->getQuotedName($table . '_' . $pkName . '_seq')); } return $lastSequence; } /** * Returns the primary key(s) of the table, based on: * https://wiki.postgresql.org/wiki/Retrieve_primary_key_columns. * * @param string $tableName * * @return array[string] */ public function getPrimaryKey($tableName) { if (!isset($this->primaryKeys[$tableName])) { $primaryKey = []; $query = "SELECT a.attname FROM pg_index i JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = ANY(i.indkey) WHERE i.indrelid = '$tableName'::regclass AND i.indisprimary"; $stmt = $this->executeQuery($query, []); $columns = $stmt->fetchAll(\PDO::FETCH_ASSOC); foreach ($columns as $column) { $primaryKey []= $column['attname']; } $this->primaryKeys[$tableName] = $primaryKey; } return $this->primaryKeys[$tableName]; } } filename = Configuration::projectDir() . $filename; $this->dsn = 'sqlite:' . $this->filename; parent::__construct($this->dsn, $user, $password, $options); } public function cleanup() { $this->dbh = null; file_put_contents($this->filename, ''); $this->dbh = self::connect($this->dsn, $this->user, $this->password); } public function load($sql) { if ($this->hasSnapshot) { $this->dbh = null; file_put_contents($this->filename, file_get_contents($this->filename . '_snapshot')); $this->dbh = new \PDO($this->dsn, $this->user, $this->password); } else { if (file_exists($this->filename . '_snapshot')) { unlink($this->filename . '_snapshot'); } parent::load($sql); copy($this->filename, $this->filename . '_snapshot'); $this->hasSnapshot = true; } } /** * @param string $tableName * * @return array[string] */ public function getPrimaryKey($tableName) { if (!isset($this->primaryKeys[$tableName])) { if ($this->hasRowId($tableName)) { return $this->primaryKeys[$tableName] = ['_ROWID_']; } $primaryKey = []; $query = 'PRAGMA table_info(' . $this->getQuotedName($tableName) . ')'; $stmt = $this->executeQuery($query, []); $columns = $stmt->fetchAll(\PDO::FETCH_ASSOC); foreach ($columns as $column) { if ($column['pk'] !== '0') { $primaryKey []= $column['name']; } } $this->primaryKeys[$tableName] = $primaryKey; } return $this->primaryKeys[$tableName]; } /** * @param $tableName * @return bool */ private function hasRowId($tableName) { $params = ['type' => 'table', 'name' => $tableName]; $select = $this->select('sql', 'sqlite_master', $params); $result = $this->executeQuery($select, $params); $sql = $result->fetchColumn(0); return strpos($sql, ') WITHOUT ROWID') === false; } } dsn, $matches); if (!$matched) { return false; } return $matches[1]; } public function cleanup() { $this->dbh->exec( " DECLARE constraints_cursor CURSOR FOR SELECT name, parent_object_id FROM sys.foreign_keys; OPEN constraints_cursor DECLARE @constraint sysname; DECLARE @parent int; DECLARE @table nvarchar(128); FETCH NEXT FROM constraints_cursor INTO @constraint, @parent; WHILE (@@FETCH_STATUS <> -1) BEGIN SET @table = OBJECT_NAME(@parent) EXEC ('ALTER TABLE [' + @table + '] DROP CONSTRAINT [' + @constraint + ']') FETCH NEXT FROM constraints_cursor INTO @constraint, @parent; END DEALLOCATE constraints_cursor;" ); $this->dbh->exec( " DECLARE tables_cursor CURSOR FOR SELECT name FROM sysobjects WHERE type = 'U'; OPEN tables_cursor DECLARE @tablename sysname; FETCH NEXT FROM tables_cursor INTO @tablename; WHILE (@@FETCH_STATUS <> -1) BEGIN EXEC ('DROP TABLE [' + @tablename + ']') FETCH NEXT FROM tables_cursor INTO @tablename; END DEALLOCATE tables_cursor;" ); } public function getQuotedName($name) { return '[' . str_replace('.', '].[', $name) . ']'; } /** * @param string $tableName * * @return array[string] */ public function getPrimaryKey($tableName) { if (!isset($this->primaryKeys[$tableName])) { $primaryKey = []; $query = " SELECT Col.Column_Name from INFORMATION_SCHEMA.TABLE_CONSTRAINTS Tab, INFORMATION_SCHEMA.CONSTRAINT_COLUMN_USAGE Col WHERE Col.Constraint_Name = Tab.Constraint_Name AND Col.Table_Name = Tab.Table_Name AND Constraint_Type = 'PRIMARY KEY' AND Col.Table_Name = ?"; $stmt = $this->executeQuery($query, [$tableName]); $columns = $stmt->fetchAll(\PDO::FETCH_ASSOC); foreach ($columns as $column) { $primaryKey []= $column['Column_Name']; } $this->primaryKeys[$tableName] = $primaryKey; } return $this->primaryKeys[$tableName]; } } internalDomains = null; } } name = $name; $this->actor = $actor; $this->multiSessionModules = array_filter($modules, function ($m) { return $m instanceof Interfaces\MultiSession; }); if (empty($this->multiSessionModules)) { throw new TestRuntimeException("No multisession modules used. Can't instantiate friend"); } } public function does($closure) { $currentUserData = []; foreach ($this->multiSessionModules as $module) { $name = $module->_getName(); $currentUserData[$name] = $module->_backupSession(); if (empty($this->data[$name])) { $module->_initializeSession(); $this->data[$name] = $module->_backupSession(); continue; } $module->_loadSession($this->data[$name]); }; $this->actor->comment(strtoupper("{$this->name} does ---")); $ret = $closure($this->actor); $this->actor->comment(strtoupper("--- {$this->name} finished")); foreach ($this->multiSessionModules as $module) { $name = $module->_getName(); $this->data[$name] = $module->_backupSession(); $module->_loadSession($currentUserData[$name]); }; return $ret; } public function isGoingTo($argumentation) { $this->actor->amGoingTo($argumentation); } public function expects($prediction) { $this->actor->expect($prediction); } public function expectsTo($prediction) { $this->actor->expectTo($prediction); } public function leave() { foreach ($this->multiSessionModules as $module) { if (isset($this->data[$module->_getName()])) { $module->_closeSession($this->data[$module->_getName()]); } } } } getScenario()->runStep(new \Codeception\Step\{{step}}('{{method}}', func_get_args())); } EOF; protected $name; protected $settings; protected $modules = []; protected $actions; protected $numMethods = 0; /** * @var array GeneratedStep[] */ protected $generatedSteps = []; public function __construct($settings) { $this->name = $settings['actor']; $this->settings = $settings; $this->di = new Di(); $modules = Configuration::modules($this->settings); $this->moduleContainer = new ModuleContainer($this->di, $settings); foreach ($modules as $moduleName) { $this->moduleContainer->create($moduleName); } $this->modules = $this->moduleContainer->all(); $this->actions = $this->moduleContainer->getActions(); $this->generatedSteps = (array) $settings['step_decorators']; } public function produce() { $namespace = rtrim($this->settings['namespace'], '\\'); $methods = []; $code = []; foreach ($this->actions as $action => $moduleName) { if (in_array($action, $methods)) { continue; } $class = new \ReflectionClass($this->modules[$moduleName]); $method = $class->getMethod($action); $code[] = $this->addMethod($method); $methods[] = $action; $this->numMethods++; } return (new Template($this->template)) ->place('namespace', $namespace ? $namespace . '\\' : '') ->place('hash', self::genHash($this->modules, $this->settings)) ->place('name', $this->name) ->place('methods', implode("\n\n ", $code)) ->produce(); } protected function addMethod(\ReflectionMethod $refMethod) { $class = $refMethod->getDeclaringClass(); $params = $this->getParamsString($refMethod); $module = $class->getName(); $body = ''; $doc = $this->addDoc($class, $refMethod); $doc = str_replace('/**', '', $doc); $doc = trim(str_replace('*/', '', $doc)); if (!$doc) { $doc = "*"; } $methodTemplate = (new Template($this->methodTemplate)) ->place('module', $module) ->place('method', $refMethod->name) ->place('params', $params); if (0 === strpos($refMethod->name, 'see')) { $type = 'Assertion'; } elseif (0 === strpos($refMethod->name, 'am')) { $type = 'Condition'; } else { $type = 'Action'; } $body .= $methodTemplate ->place('doc', $doc) ->place('action', $refMethod->name) ->place('step', $type) ->produce(); // add auto generated steps foreach (array_unique($this->generatedSteps) as $generator) { if (!is_callable([$generator, 'getTemplate'])) { throw new \Exception("Wrong configuration for generated steps. $generator doesn't implement \Codeception\Step\GeneratedStep interface"); } $template = call_user_func([$generator, 'getTemplate'], clone $methodTemplate); if ($template) { $body .= $template->produce(); } } return $body; } /** * @param \ReflectionMethod $refMethod * @return array */ protected function getParamsString(\ReflectionMethod $refMethod) { $params = []; foreach ($refMethod->getParameters() as $param) { if ($param->isOptional()) { $params[] = '$' . $param->name . ' = null'; } else { $params[] = '$' . $param->name; }; } return implode(', ', $params); } /** * @param \ReflectionClass $class * @param \ReflectionMethod $refMethod * @return string */ protected function addDoc(\ReflectionClass $class, \ReflectionMethod $refMethod) { $doc = $refMethod->getDocComment(); if (!$doc) { $interfaces = $class->getInterfaces(); foreach ($interfaces as $interface) { $i = new \ReflectionClass($interface->name); if ($i->hasMethod($refMethod->name)) { $doc = $i->getMethod($refMethod->name)->getDocComment(); break; } } } if (!$doc and $class->getParentClass()) { $parent = new \ReflectionClass($class->getParentClass()->name); if ($parent->hasMethod($refMethod->name)) { $doc = $parent->getMethod($refMethod->name)->getDocComment(); return $doc; } return $doc; } return $doc; } public static function genHash($modules, $settings) { $actions = []; foreach ($modules as $moduleName => $module) { $actions[$moduleName] = get_class_methods(get_class($module)); } return md5(Codecept::VERSION . serialize($actions) . serialize($settings['modules']) . implode(',', (array) $settings['step_decorators'])); } public function getNumMethods() { return $this->numMethods; } } settings = $settings; $this->di = new Di(); $this->moduleContainer = new ModuleContainer($this->di, $settings); $modules = Configuration::modules($this->settings); foreach ($modules as $moduleName) { $this->moduleContainer->create($moduleName); } $this->modules = $this->moduleContainer->all(); $this->actions = $this->moduleContainer->getActions(); } public function produce() { $namespace = rtrim($this->settings['namespace'], '\\'); if (!isset($this->settings['actor']) && isset($this->settings['class_name'])) { $this->settings['actor'] = $this->settings['class_name']; } return (new Template($this->template)) ->place('hasNamespace', $namespace ? "namespace $namespace;" : '') ->place('actor', $this->settings['actor']) ->place('inheritedMethods', $this->prependAbstractActorDocBlocks()) ->produce(); } protected function prependAbstractActorDocBlocks() { $inherited = []; $class = new \ReflectionClass('\Codeception\\Actor'); $methods = $class->getMethods(\ReflectionMethod::IS_PUBLIC); foreach ($methods as $method) { if ($method->name == '__call') { continue; } // skipping magic if ($method->name == '__construct') { continue; } // skipping magic $returnType = 'void'; if ($method->name == 'haveFriend') { $returnType = '\Codeception\Lib\Friend'; } $params = $this->getParamsString($method); $inherited[] = (new Template($this->inheritedMethodTemplate)) ->place('method', $method->name) ->place('params', $params) ->place('return', $returnType) ->produce(); } return implode("\n", $inherited); } /** * @param \ReflectionMethod $refMethod * @return array */ protected function getParamsString(\ReflectionMethod $refMethod) { $params = []; foreach ($refMethod->getParameters() as $param) { if ($param->isOptional()) { $params[] = '$' . $param->name . ' = '.$this->getDefaultValue($param); } else { $params[] = '$' . $param->name; }; } return implode(', ', $params); } public function getActorName() { return $this->settings['actor']; } public function getModules() { return array_keys($this->modules); } /** * Infer default parameter from the reflection object and format it as PHP (code) string * * @param \ReflectionParameter $param * * @return string */ private function getDefaultValue(\ReflectionParameter $param) { if ($param->isDefaultValueAvailable()) { if (method_exists($param, 'isDefaultValueConstant') && $param->isDefaultValueConstant()) { $constName = $param->getDefaultValueConstantName(); if (false !== strpos($constName, '::')) { list($class, $const) = explode('::', $constName); if (in_array($class, ['self', 'static'])) { $constName = $param->getDeclaringClass()->getName().'::'.$const; } } return $constName; } return $this->phpEncodeValue($param->getDefaultValue()); } return 'null'; } /** * PHP encoded a value * * @param mixed $value * * @return string */ private function phpEncodeValue($value) { if (is_array($value)) { return $this->phpEncodeArray($value); } if (is_string($value)) { return json_encode($value); } return var_export($value, true); } /** * Recursively PHP encode an array * * @param array $array * * @return string */ private function phpEncodeArray(array $array) { $isPlainArray = function (array $value) { return ((count($value) === 0) || ( (array_keys($value) === range(0, count($value) - 1)) && (0 === count(array_filter(array_keys($value), 'is_string')))) ); }; if ($isPlainArray($array)) { return '['.implode(', ', array_map([$this, 'phpEncodeValue'], $array)).']'; } return '['.implode(', ', array_map(function ($key) use ($array) { return $this->phpEncodeValue($key).' => '.$this->phpEncodeValue($array[$key]); }, array_keys($array))).']'; } } wantTo('perform actions and see result'); EOF; protected $settings; public function __construct($settings) { $this->settings = $settings; } public function produce() { $actor = $this->settings['actor']; if (!$actor) { throw new ConfigurationException("Cept can't be created for suite without an actor. Add `actor: SomeTester` to suite config"); } $use = ''; if (! empty($this->settings['namespace'])) { $namespace = rtrim($this->settings['namespace'], '\\'); $use = "use {$namespace}\\$actor;"; } return (new Template($this->template)) ->place('actor', $actor) ->place('use', $use) ->produce(); } } name = $this->removeSuffix($className, 'Cest'); $this->settings = $settings; } public function produce() { $actor = $this->settings['actor']; if (!$actor) { throw new ConfigurationException("Cept can't be created for suite without an actor. Add `actor: SomeTester` to suite config"); } if (array_key_exists('suite_namespace', $this->settings)) { $namespace = rtrim($this->settings['suite_namespace'], '\\'); } else { $namespace = rtrim($this->settings['namespace'], '\\'); } $ns = $this->getNamespaceHeader($namespace.'\\'.$this->name); if ($namespace) { $ns .= "use ".$this->settings['namespace'].'\\'.$actor.";"; } return (new Template($this->template)) ->place('name', $this->getShortClassName($this->name)) ->place('namespace', $ns) ->place('actor', $actor) ->produce(); } } name = $name; } public function produce() { return (new Template($this->template)) ->place('name', $this->name) ->produce(); } } getPattern(); $path = $settings['path']; if (!empty($test)) { $path = $settings['path'].'/'.$test; if (preg_match($pattern, $test)) { $path = dirname($path); $pattern = basename($test); } } $finder = Finder::create() ->files() ->sortByName() ->in($path) ->followLinks() ->name($pattern); foreach ($finder as $file) { $pathname = str_replace("//", "/", $file->getPathname()); $loader->loadTests($pathname); } $availableSteps = $loader->getSteps(); $allSteps = []; foreach ($availableSteps as $stepGroup) { $allSteps = array_merge($allSteps, $stepGroup); } foreach ($loader->getTests() as $test) { /** @var $test \Codeception\Test\Gherkin **/ $steps = $test->getScenarioNode()->getSteps(); if ($test->getFeatureNode()->hasBackground()) { $steps = array_merge($steps, $test->getFeatureNode()->getBackground()->getSteps()); } foreach ($steps as $step) { $matched = false; $text = $step->getText(); if (self::stepHasPyStringArgument($step)) { // pretend it is inline argument $text .= ' ""'; } foreach (array_keys($allSteps) as $pattern) { if (preg_match($pattern, $text)) { $matched = true; break; } } if (!$matched) { $this->addSnippet($step); $file = str_ireplace($settings['path'], '', $test->getFeatureNode()->getFile()); if (!in_array($file, $this->features)) { $this->features[] = $file; } } } } } public function addSnippet(StepNode $step) { $args = []; $pattern = $step->getText(); // match numbers (not in quotes) if (preg_match_all('~([\d\.])(?=([^"]*"[^"]*")*[^"]*$)~', $pattern, $matches)) { foreach ($matches[1] as $num => $param) { $num++; $args[] = '$num' . $num; $pattern = str_replace($param, ":num$num", $pattern); } } // match quoted string if (preg_match_all('~"(.*?)"~', $pattern, $matches)) { foreach ($matches[1] as $num => $param) { $num++; $args[] = '$arg' . $num; $pattern = str_replace('"'.$param.'"', ":arg$num", $pattern); } } // Has multiline argument at the end of step? if (self::stepHasPyStringArgument($step)) { $num = count($args) + 1; $pattern .= " :arg$num"; $args[] = '$arg' . $num; } if (in_array($pattern, $this->processed)) { return; } $methodName = preg_replace('~(\s+?|\'|\"|\W)~', '', ucwords(preg_replace('~"(.*?)"|\d+~', '', $step->getText()))); if (empty($methodName)) { $methodName = 'step_' . substr(sha1($pattern), 0, 9); } $this->snippets[] = (new Template($this->template)) ->place('type', $step->getKeywordType()) ->place('text', $pattern) ->place('methodName', lcfirst($methodName)) ->place('params', implode(', ', $args)) ->produce(); $this->processed[] = $pattern; } public function getSnippets() { return $this->snippets; } public function getFeatures() { return $this->features; } public static function stepHasPyStringArgument(StepNode $step) { if ($step->hasArguments()) { $stepArgs = $step->getArguments(); if ($stepArgs[count($stepArgs) - 1]->getNodeType() == "PyString") { return true; } } return false; } } settings = $settings; $this->name = $name; $this->namespace = $this->getNamespaceString($this->settings['namespace'] . '\\Group\\' . $name); } public function produce() { $ns = $this->getNamespaceString($this->settings['namespace'] . '\\' . $this->name); return (new Template($this->template)) ->place('class', ucfirst($this->name)) ->place('name', $this->name) ->place('namespace', $this->namespace) ->place('groupName', strtolower($this->name)) ->produce(); } } namespace = $namespace; $this->name = $name; } public function produce() { return (new Template($this->template)) ->place('namespace', $this->getNamespaceHeader($this->namespace . '\\Helper\\' . $this->name)) ->place('name', $this->getShortClassName($this->name)) ->produce(); } public function getHelperName() { return rtrim('\\' . $this->namespace, '\\') . '\\Helper\\' . $this->name; } } {{actor}} = \$I; } EOF; protected $actions = ''; protected $settings; protected $name; protected $namespace; public function __construct($settings, $name) { $this->settings = $settings; $this->name = $this->getShortClassName($name); $this->namespace = $this->getNamespaceString($this->settings['namespace'] . '\\Page\\' . $name); } public function produce() { return (new Template($this->template)) ->place('namespace', $this->namespace) ->place('actions', $this->produceActions()) ->place('class', $this->name) ->produce(); } protected function produceActions() { if (!isset($this->settings['actor'])) { return ''; // global pageobject } $actor = lcfirst($this->settings['actor']); $actorClass = $this->settings['actor']; if (!empty($this->settings['namespace'])) { $actorClass = rtrim($this->settings['namespace'], '\\') . '\\' . $actorClass; } return (new Template($this->actionsTemplate)) ->place('actorClass', $actorClass) ->place('actor', $actor) ->place('pageObject', $this->name) ->produce(); } } {{actor}} = \$I; } EOF; protected $namespace; protected $name; protected $settings; public function __construct($settings, $name) { $this->settings = $settings; $this->name = $this->getShortClassName($name); $this->namespace = $this->getNamespaceString($this->settings['namespace'] . '\\Snapshot\\' . $name); } public function produce() { return (new Template($this->template)) ->place('namespace', $this->namespace) ->place('actions', $this->produceActions()) ->place('name', $this->name) ->produce(); } protected function produceActions() { if (!isset($this->settings['actor'])) { return ''; // no actor in suite } $actor = lcfirst($this->settings['actor']); $actorClass = $this->settings['actor']; if (!empty($this->settings['namespace'])) { $actorClass = rtrim($this->settings['namespace'], '\\') . '\\' . $actorClass; } return (new Template($this->actionsTemplate)) ->place('actorClass', $actorClass) ->place('actor', $actor) ->produce(); } } settings = $settings; $this->name = $this->getShortClassName($name); $this->namespace = $this->getNamespaceString($this->settings['namespace'] . '\\Step\\' . $name); } public function produce() { $actor = $this->settings['actor']; if (!$actor) { throw new ConfigurationException("Steps can't be created for suite without an actor"); } $ns = $this->getNamespaceString($this->settings['namespace'] . '\\' . $actor . '\\' . $this->name); $ns = ltrim($ns, '\\'); $extended = '\\' . ltrim('\\' . $this->settings['namespace'] . '\\' . $actor, '\\'); return (new Template($this->template)) ->place('namespace', $this->namespace) ->place('name', $this->name) ->place('actorClass', $extended) ->place('actions', $this->actions) ->produce(); } public function createAction($action) { $this->actions .= (new Template($this->actionTemplate)) ->place('action', $action) ->produce(); } } settings = $settings; $this->name = $this->removeSuffix($name, 'Test'); } public function produce() { $actor = $this->settings['actor']; if ($this->settings['namespace']) { $actor = $this->settings['namespace'] . '\\' . $actor; } $ns = $this->getNamespaceHeader($this->settings['namespace'] . '\\' . $this->name); $tester = ''; if ($this->settings['actor']) { $tester = (new Template($this->testerTemplate)) ->place('actorClass', $actor) ->place('actor', lcfirst(Configuration::config()['actor_suffix'])) ->produce(); } return (new Template($this->template)) ->place('namespace', $ns) ->place('name', $this->getShortClassName($this->name)) ->place('tester', $tester) ->produce(); } } configuredGroups = $groups; $this->loadGroupsByPattern(); $this->loadConfiguredGroupSettings(); } /** * proceeds group names with asterisk: * * ``` * "tests/_log/g_*" => [ * "tests/_log/group_1", * "tests/_log/group_2", * "tests/_log/group_3", * ] * ``` */ protected function loadGroupsByPattern() { foreach ($this->configuredGroups as $group => $pattern) { if (strpos($group, '*') === false) { continue; } $files = Finder::create()->files() ->name(basename($pattern)) ->sortByName() ->in(Configuration::projectDir().dirname($pattern)); $i = 1; foreach ($files as $file) { /** @var SplFileInfo $file * */ $this->configuredGroups[str_replace('*', $i, $group)] = dirname($pattern).DIRECTORY_SEPARATOR.$file->getRelativePathname(); $i++; } unset($this->configuredGroups[$group]); } } protected function loadConfiguredGroupSettings() { foreach ($this->configuredGroups as $group => $tests) { $this->testsInGroups[$group] = []; if (is_array($tests)) { foreach ($tests as $test) { $file = str_replace(['/', '\\'], [DIRECTORY_SEPARATOR, DIRECTORY_SEPARATOR], $test); $this->testsInGroups[$group][] = Configuration::projectDir() . $file; } } elseif (is_file(Configuration::projectDir() . $tests)) { $handle = @fopen(Configuration::projectDir() . $tests, "r"); if ($handle) { while (($test = fgets($handle, 4096)) !== false) { // if the current line is blank then we need to move to the next line // otherwise the current codeception directory becomes part of the group // which causes every single test to run if (trim($test) === '') { continue; } $file = trim(Configuration::projectDir() . $test); $file = str_replace(['/', '\\'], [DIRECTORY_SEPARATOR, DIRECTORY_SEPARATOR], $file); $this->testsInGroups[$group][] = $file; } fclose($handle); } } } } public function groupsForTest(\PHPUnit\Framework\Test $test) { $groups = []; $filename = Descriptor::getTestFileName($test); if ($test instanceof TestInterface) { $groups = $test->getMetadata()->getGroups(); } if ($test instanceof Reported) { $info = $test->getReportFields(); if (isset($info['class'])) { $groups = array_merge($groups, \PHPUnit\Util\Test::getGroups($info['class'], $info['name'])); } $filename = str_replace(['\\\\', '//'], ['\\', '/'], $info['file']); } if ($test instanceof \PHPUnit\Framework\TestCase) { $groups = array_merge($groups, \PHPUnit\Util\Test::getGroups(get_class($test), $test->getName(false))); } if ($test instanceof \PHPUnit\Framework\TestSuite\DataProvider) { $firstTest = $test->testAt(0); if ($firstTest != false && $firstTest instanceof TestInterface) { $groups = array_merge($groups, $firstTest->getMetadata()->getGroups()); $filename = Descriptor::getTestFileName($firstTest); } } foreach ($this->testsInGroups as $group => $tests) { foreach ($tests as $testPattern) { if ($filename == $testPattern) { $groups[] = $group; } if (strpos($filename . ':' . $test->getName(false), $testPattern) === 0) { $groups[] = $group; } if ($test instanceof Gherkin && mb_strtolower($filename . ':' . $test->getMetadata()->getFeature()) === mb_strtolower($testPattern)) { $groups[] = $group; } if ($test instanceof \PHPUnit\Framework\TestSuite\DataProvider) { $firstTest = $test->testAt(0); if ($firstTest != false && $firstTest instanceof TestInterface) { if (strpos($filename . ':' . $firstTest->getName(false), $testPattern) === 0) { $groups[] = $group; } } } } } return array_unique($groups); } } null, 'path' => '/', 'domain' => '', 'secure' => false]; protected $internalDomains = null; private $baseUrl; public function _failed(TestInterface $test, $fail) { if (!$this->client || !$this->client->getInternalResponse()) { return; } $filename = preg_replace('~\W~', '.', Descriptor::getTestSignatureUnique($test)); $extensions = ['application/json' => 'json', 'text/xml' => 'xml', 'application/xml' => 'xml']; $internalResponse = $this->client->getInternalResponse(); $responseContentType = $internalResponse ? $internalResponse->getHeader('content-type') : null; $extension = isset($extensions[$responseContentType]) ? $extensions[$responseContentType] : 'html'; $filename = mb_strcut($filename, 0, 244, 'utf-8') . '.fail.' . $extension; $this->_savePageSource($report = codecept_output_dir() . $filename); $test->getMetadata()->addReport('response', $report); } public function _after(TestInterface $test) { $this->client = null; $this->crawler = null; $this->forms = []; $this->headers = []; } public function _conflicts() { return 'Codeception\Lib\Interfaces\Web'; } public function _findElements($locator) { return $this->match($locator); } /** * Send custom request to a backend using method, uri, parameters, etc. * Use it in Helpers to create special request actions, like accessing API * Returns a string with response body. * * ```php * getModule('{{MODULE_NAME}}')->_request('POST', '/api/v1/users', ['name' => $name]); * $user = json_decode($userData); * return $user->id; * } * ?> * ``` * Does not load the response into the module so you can't interact with response page (click, fill forms). * To load arbitrary page for interaction, use `_loadPage` method. * * @api * @param $method * @param $uri * @param array $parameters * @param array $files * @param array $server * @param null $content * @return mixed|Crawler * @throws ExternalUrlException * @see `_loadPage` */ public function _request( $method, $uri, array $parameters = [], array $files = [], array $server = [], $content = null ) { $this->clientRequest($method, $uri, $parameters, $files, $server, $content, true); return $this->_getResponseContent(); } /** * Returns content of the last response * Use it in Helpers when you want to retrieve response of request performed by another module. * * ```php * assertStringContainsString($text, $this->getModule('{{MODULE_NAME}}')->_getResponseContent(), "response contains"); * } * ?> * ``` * * @api * @return string * @throws ModuleException */ public function _getResponseContent() { return (string)$this->getRunningClient()->getInternalResponse()->getContent(); } protected function clientRequest($method, $uri, array $parameters = [], array $files = [], array $server = [], $content = null, $changeHistory = true) { $this->debugSection("Request Headers", $this->headers); foreach ($this->headers as $header => $val) { // moved from REST module if ($val === null || $val === '') { continue; } $header = str_replace('-', '_', strtoupper($header)); $server["HTTP_$header"] = $val; // Issue #827 - symfony foundation requires 'CONTENT_TYPE' without HTTP_ if ($this instanceof Framework && $header === 'CONTENT_TYPE') { $server[$header] = $val; } } $server['REQUEST_TIME'] = time(); $server['REQUEST_TIME_FLOAT'] = microtime(true); if ($this instanceof Framework) { if (preg_match('#^(//|https?://(?!localhost))#', $uri)) { $hostname = parse_url($uri, PHP_URL_HOST); if (!$this->isInternalDomain($hostname)) { throw new ExternalUrlException(get_class($this) . " can't open external URL: " . $uri); } } if ($method !== 'GET' && $content === null && !empty($parameters)) { $content = http_build_query($parameters); } } if (!ReflectionHelper::readPrivateProperty($this->client, 'followRedirects')) { $result = $this->client->request($method, $uri, $parameters, $files, $server, $content, $changeHistory); $this->debugResponse($uri); return $result; } $maxRedirects = ReflectionHelper::readPrivateProperty($this->client, 'maxRedirects', 'Symfony\Component\BrowserKit\Client'); $this->client->followRedirects(false); $result = $this->client->request($method, $uri, $parameters, $files, $server, $content, $changeHistory); $this->debugResponse($uri); return $this->redirectIfNecessary($result, $maxRedirects, 0); } protected function isInternalDomain($domain) { if ($this->internalDomains === null) { $this->internalDomains = $this->getInternalDomains(); } foreach ($this->internalDomains as $pattern) { if (preg_match($pattern, $domain)) { return true; } } return false; } /** * Opens a page with arbitrary request parameters. * Useful for testing multi-step forms on a specific step. * * ```php * getModule('{{MODULE_NAME}}')->_loadPage('POST', '/checkout/step2', ['order' => $orderId]); * } * ?> * ``` * * @api * @param $method * @param $uri * @param array $parameters * @param array $files * @param array $server * @param null $content */ public function _loadPage( $method, $uri, array $parameters = [], array $files = [], array $server = [], $content = null ) { $this->crawler = $this->clientRequest($method, $uri, $parameters, $files, $server, $content); $this->baseUrl = $this->retrieveBaseUrl(); $this->forms = []; } /** * @return Crawler * @throws ModuleException */ private function getCrawler() { if (!$this->crawler) { throw new ModuleException($this, 'Crawler is null. Perhaps you forgot to call "amOnPage"?'); } return $this->crawler; } private function getRunningClient() { if ($this->client->getInternalRequest() === null) { throw new ModuleException( $this, "Page not loaded. Use `\$I->amOnPage` (or hidden API methods `_request` and `_loadPage`) to open it" ); } return $this->client; } public function _savePageSource($filename) { file_put_contents($filename, $this->_getResponseContent()); } /** * Authenticates user for HTTP_AUTH * * @param $username * @param $password */ public function amHttpAuthenticated($username, $password) { $this->client->setServerParameter('PHP_AUTH_USER', $username); $this->client->setServerParameter('PHP_AUTH_PW', $password); } /** * Sets the HTTP header to the passed value - which is used on * subsequent HTTP requests through PhpBrowser. * * Example: * ```php * haveHttpHeader('X-Requested-With', 'Codeception'); * $I->amOnPage('test-headers.php'); * ?> * ``` * * To use special chars in Header Key use HTML Character Entities: * Example: * Header with underscore - 'Client_Id' * should be represented as - 'Client_Id' or 'Client_Id' * * ```php * haveHttpHeader('Client_Id', 'Codeception'); * ?> * ``` * * @param string $name the name of the request header * @param string $value the value to set it to for subsequent * requests */ public function haveHttpHeader($name, $value) { $name = implode('-', array_map('ucfirst', explode('-', strtolower(str_replace('_', '-', $name))))); $this->headers[$name] = $value; } /** * Deletes the header with the passed name. Subsequent requests * will not have the deleted header in its request. * * Example: * ```php * haveHttpHeader('X-Requested-With', 'Codeception'); * $I->amOnPage('test-headers.php'); * // ... * $I->deleteHeader('X-Requested-With'); * $I->amOnPage('some-other-page.php'); * ?> * ``` * * @param string $name the name of the header to delete. */ public function deleteHeader($name) { $name = implode('-', array_map('ucfirst', explode('-', strtolower(str_replace('_', '-', $name))))); unset($this->headers[$name]); } public function amOnPage($page) { $this->_loadPage('GET', $page); } public function click($link, $context = null) { if ($context) { $this->crawler = $this->match($context); } if (is_array($link)) { $this->clickByLocator($link); return; } $anchor = $this->strictMatch(['link' => $link]); if (!count($anchor)) { $anchor = $this->getCrawler()->selectLink($link); } if (count($anchor)) { $this->openHrefFromDomNode($anchor->getNode(0)); return; } $buttonText = str_replace('"', "'", $link); $button = $this->crawler->selectButton($buttonText); if (count($button) && $this->clickButton($button->getNode(0))) { return; } try { $this->clickByLocator($link); } catch (MalformedLocatorException $e) { throw new ElementNotFound("name=$link", "'$link' is invalid CSS and XPath selector and Link or Button"); } } /** * @param $link * @return bool */ protected function clickByLocator($link) { $nodes = $this->match($link); if (!$nodes->count()) { throw new ElementNotFound($link, 'Link or Button by name or CSS or XPath'); } foreach ($nodes as $node) { $tag = $node->tagName; $type = $node->getAttribute('type'); if ($tag === 'a') { $this->openHrefFromDomNode($node); return true; } elseif (in_array($tag, ['input', 'button']) && in_array($type, ['submit', 'image'])) { return $this->clickButton($node); } } } /** * Clicks the link or submits the form when the button is clicked * @param \DOMNode $node * @return boolean clicked something */ private function clickButton(\DOMNode $node) { $formParams = []; $buttonName = (string)$node->getAttribute('name'); $buttonValue = $node->getAttribute('value'); if ($buttonName !== '' && $buttonValue !== null) { $formParams = [$buttonName => $buttonValue]; } while ($node->parentNode !== null) { $node = $node->parentNode; if (!isset($node->tagName)) { // this is the top most node, it has no parent either break; } if ($node->tagName === 'a') { $this->openHrefFromDomNode($node); return true; } elseif ($node->tagName === 'form') { $this->proceedSubmitForm( new Crawler($node, $this->getAbsoluteUrlFor($this->_getCurrentUri()), $this->getBaseUrl()), $formParams ); return true; } } throw new TestRuntimeException('Button is not inside a link or a form'); } private function openHrefFromDomNode(\DOMNode $node) { $link = new Link($node, $this->getBaseUrl()); $this->amOnPage(preg_replace('/#.*/', '', $link->getUri())); } private function getBaseUrl() { return $this->baseUrl; } private function retrieveBaseUrl() { $baseUrl = ''; $baseHref = $this->crawler->filter('base'); if (count($baseHref) > 0) { $baseUrl = $baseHref->getNode(0)->getAttribute('href'); } if ($baseUrl == '') { $baseUrl = $this->_getCurrentUri(); } return $this->getAbsoluteUrlFor($baseUrl); } public function see($text, $selector = null) { if (!$selector) { $this->assertPageContains($text); return; } $nodes = $this->match($selector); $this->assertDomContains($nodes, $this->stringifySelector($selector), $text); } public function dontSee($text, $selector = null) { if (!$selector) { $this->assertPageNotContains($text); return; } $nodes = $this->match($selector); $this->assertDomNotContains($nodes, $this->stringifySelector($selector), $text); } public function seeInSource($raw) { $this->assertPageSourceContains($raw); } public function dontSeeInSource($raw) { $this->assertPageSourceNotContains($raw); } public function seeLink($text, $url = null) { $crawler = $this->getCrawler()->selectLink($text); if ($crawler->count() === 0) { $this->fail("No links containing text '$text' were found in page " . $this->_getCurrentUri()); } if ($url) { $crawler = $crawler->filterXPath(sprintf('.//a[substring(@href, string-length(@href) - string-length(%1$s) + 1)=%1$s]', Crawler::xpathLiteral($url))); if ($crawler->count() === 0) { $this->fail("No links containing text '$text' and URL '$url' were found in page " . $this->_getCurrentUri()); } } $this->assertTrue(true); } public function dontSeeLink($text, $url = null) { $crawler = $this->getCrawler()->selectLink($text); if (!$url) { if ($crawler->count() > 0) { $this->fail("Link containing text '$text' was found in page " . $this->_getCurrentUri()); } } $crawler = $crawler->filterXPath(sprintf('.//a[substring(@href, string-length(@href) - string-length(%1$s) + 1)=%1$s]', Crawler::xpathLiteral($url))); if ($crawler->count() > 0) { $this->fail("Link containing text '$text' and URL '$url' was found in page " . $this->_getCurrentUri()); } } /** * @return string * @throws ModuleException */ public function _getCurrentUri() { return Uri::retrieveUri($this->getRunningClient()->getHistory()->current()->getUri()); } public function seeInCurrentUrl($uri) { $this->assertStringContainsString($uri, $this->_getCurrentUri()); } public function dontSeeInCurrentUrl($uri) { $this->assertStringNotContainsString($uri, $this->_getCurrentUri()); } public function seeCurrentUrlEquals($uri) { $this->assertEquals(rtrim($uri, '/'), rtrim($this->_getCurrentUri(), '/')); } public function dontSeeCurrentUrlEquals($uri) { $this->assertNotEquals(rtrim($uri, '/'), rtrim($this->_getCurrentUri(), '/')); } public function seeCurrentUrlMatches($uri) { \PHPUnit\Framework\Assert::assertRegExp($uri, $this->_getCurrentUri()); } public function dontSeeCurrentUrlMatches($uri) { \PHPUnit\Framework\Assert::assertNotRegExp($uri, $this->_getCurrentUri()); } public function grabFromCurrentUrl($uri = null) { if (!$uri) { return $this->_getCurrentUri(); } $matches = []; $res = preg_match($uri, $this->_getCurrentUri(), $matches); if (!$res) { $this->fail("Couldn't match $uri in " . $this->_getCurrentUri()); } if (!isset($matches[1])) { $this->fail("Nothing to grab. A regex parameter required. Ex: '/user/(\\d+)'"); } return $matches[1]; } public function seeCheckboxIsChecked($checkbox) { $checkboxes = $this->getFieldsByLabelOrCss($checkbox); $this->assertDomContains($checkboxes->filter('input[checked=checked]'), 'checkbox'); } public function dontSeeCheckboxIsChecked($checkbox) { $checkboxes = $this->getFieldsByLabelOrCss($checkbox); $this->assertEquals(0, $checkboxes->filter('input[checked=checked]')->count()); } public function seeInField($field, $value) { $nodes = $this->getFieldsByLabelOrCss($field); $this->assert($this->proceedSeeInField($nodes, $value)); } public function dontSeeInField($field, $value) { $nodes = $this->getFieldsByLabelOrCss($field); $this->assertNot($this->proceedSeeInField($nodes, $value)); } public function seeInFormFields($formSelector, array $params) { $this->proceedSeeInFormFields($formSelector, $params, false); } public function dontSeeInFormFields($formSelector, array $params) { $this->proceedSeeInFormFields($formSelector, $params, true); } protected function proceedSeeInFormFields($formSelector, array $params, $assertNot) { $form = $this->match($formSelector)->first(); if ($form->count() === 0) { throw new ElementNotFound($formSelector, 'Form'); } $fields = []; foreach ($params as $name => $values) { $this->pushFormField($fields, $form, $name, $values); } foreach ($fields as $element) { list($field, $values) = $element; if (!is_array($values)) { $values = [$values]; } foreach ($values as $value) { $ret = $this->proceedSeeInField($field, $value); if ($assertNot) { $this->assertNot($ret); } else { $this->assert($ret); } } } } /** * Map an array element passed to seeInFormFields to its corresponding field, * recursing through array values if the field is not found. * * @param array $fields The previously found fields. * @param Crawler $form The form in which to search for fields. * @param string $name The field's name. * @param mixed $values * @return void */ protected function pushFormField(&$fields, $form, $name, $values) { $field = $form->filterXPath(sprintf('.//*[@name=%s]', Crawler::xpathLiteral($name))); if ($field->count()) { $fields[] = [$field, $values]; } elseif (is_array($values)) { foreach ($values as $key => $value) { $this->pushFormField($fields, $form, "{$name}[$key]", $value); } } else { throw new ElementNotFound( sprintf('//*[@name=%s]', Crawler::xpathLiteral($name)), 'Form' ); } } protected function proceedSeeInField(Crawler $fields, $value) { $testValues = $this->getValueAndTextFromField($fields); if (!is_array($testValues)) { $testValues = [$testValues]; } if (is_bool($value) && $value === true && !empty($testValues)) { $value = reset($testValues); } elseif (empty($testValues)) { $testValues = ['']; } return [ 'Contains', $value, $testValues, sprintf( 'Failed asserting that `%s` is in %s\'s value: %s', $value, $fields->getNode(0)->nodeName, var_export($testValues, true) ) ]; } /** * Get the values of a set of fields and also the texts of selected options. * * @param Crawler $nodes * @return array|mixed|string */ protected function getValueAndTextFromField(Crawler $nodes) { if ($nodes->filter('textarea')->count()) { return (new TextareaFormField($nodes->filter('textarea')->getNode(0)))->getValue(); } $input = $nodes->filter('input'); if ($input->count()) { return $this->getInputValue($input); } if ($nodes->filter('select')->count()) { $options = $nodes->filter('option[selected]'); $values = []; foreach ($options as $option) { $values[] = $option->getAttribute('value'); $values[] = $option->textContent; $values[] = trim($option->textContent); } return $values; } $this->fail("Element $nodes is not a form field or does not contain a form field"); } /** * Get the values of a set of input fields. * * @param Crawler $input * @return array|string */ protected function getInputValue($input) { if ($input->attr('type') == 'checkbox' or $input->attr('type') == 'radio') { $values = []; foreach ($input->filter(':checked') as $checkbox) { $values[] = $checkbox->getAttribute('value'); } return $values; } return (new InputFormField($input->getNode(0)))->getValue(); } /** * Strips out one pair of trailing square brackets from a field's * name. * * @param string $name the field name * @return string the name after stripping trailing square brackets */ protected function getSubmissionFormFieldName($name) { if (substr($name, -2) === '[]') { return substr($name, 0, -2); } return $name; } /** * Replaces boolean values in $params with the corresponding field's * value for checkbox form fields. * * The function loops over all input checkbox fields, checking if a * corresponding key is set in $params. If it is, and the value is * boolean or an array containing booleans, the value(s) are * replaced in the array with the real value of the checkbox, and * the array is returned. * * @param Crawler $form the form to find checkbox elements * @param array $params the parameters to be submitted * @return array the $params array after replacing bool values */ protected function setCheckboxBoolValues(Crawler $form, array $params) { $checkboxes = $form->filter('input[type=checkbox]'); $chFoundByName = []; foreach ($checkboxes as $box) { $fieldName = $this->getSubmissionFormFieldName($box->getAttribute('name')); $pos = (!isset($chFoundByName[$fieldName])) ? 0 : $chFoundByName[$fieldName]; $skip = (!isset($params[$fieldName])) || (!is_array($params[$fieldName]) && !is_bool($params[$fieldName])) || (is_array($params[$fieldName]) && $pos >= count($params[$fieldName]) || (is_array($params[$fieldName]) && !is_bool($params[$fieldName][$pos]))); if ($skip) { continue; } $values = $params[$fieldName]; if ($values === true) { $params[$fieldName] = $box->hasAttribute('value') ? $box->getAttribute('value') : 'on'; $chFoundByName[$fieldName] = $pos + 1; } elseif ($values[$pos] === true) { $params[$fieldName][$pos] = $box->hasAttribute('value') ? $box->getAttribute('value') : 'on'; $chFoundByName[$fieldName] = $pos + 1; } elseif (is_array($values)) { array_splice($params[$fieldName], $pos, 1); } else { unset($params[$fieldName]); } } return $params; } /** * Submits the form currently selected in the passed Crawler, after * setting any values passed in $params and setting the value of the * passed button name. * * @param Crawler $frmCrawl the form to submit * @param array $params additional parameter values to set on the * form * @param string $button the name of a submit button in the form */ protected function proceedSubmitForm(Crawler $frmCrawl, array $params, $button = null) { $url = null; $form = $this->getFormFor($frmCrawl); $defaults = $this->getFormValuesFor($form); $merged = array_merge($defaults, $params); $requestParams = $this->setCheckboxBoolValues($frmCrawl, $merged); if (!empty($button)) { $btnCrawl = $frmCrawl->filterXPath(sprintf( '//*[not(@disabled) and @type="submit" and @name=%s]', Crawler::xpathLiteral($button) )); if (count($btnCrawl)) { $requestParams[$button] = $btnCrawl->attr('value'); $formaction = $btnCrawl->attr('formaction'); if ($formaction) { $url = $formaction; } } } if (!$url) { $url = $this->getFormUrl($frmCrawl); } if (strcasecmp($form->getMethod(), 'GET') === 0) { $url = Uri::mergeUrls($url, '?' . http_build_query($requestParams)); } $url = preg_replace('/#.*/', '', $url); $this->debugSection('Uri', $url); $this->debugSection('Method', $form->getMethod()); $this->debugSection('Parameters', $requestParams); $requestParams= $this->getFormPhpValues($requestParams); $this->crawler = $this->clientRequest( $form->getMethod(), $url, $requestParams, $form->getPhpFiles() ); $this->forms = []; } public function submitForm($selector, array $params, $button = null) { $form = $this->match($selector)->first(); if (!count($form)) { throw new ElementNotFound($this->stringifySelector($selector), 'Form'); } $this->proceedSubmitForm($form, $params, $button); } /** * Returns an absolute URL for the passed URI with the current URL * as the base path. * * @param string $uri the absolute or relative URI * @return string the absolute URL * @throws \Codeception\Exception\TestRuntimeException if either the current * URL or the passed URI can't be parsed */ protected function getAbsoluteUrlFor($uri) { $currentUrl = $this->getRunningClient()->getHistory()->current()->getUri(); if (empty($uri) || $uri[0] === '#') { return $currentUrl; } return Uri::mergeUrls($currentUrl, $uri); } /** * Returns the form action's absolute URL. * * @param \Symfony\Component\DomCrawler\Crawler $form * @return string * @throws \Codeception\Exception\TestRuntimeException if either the current * URL or the URI of the form's action can't be parsed */ protected function getFormUrl(Crawler $form) { $action = $form->form()->getUri(); return $this->getAbsoluteUrlFor($action); } /** * Returns a crawler Form object for the form pointed to by the * passed Crawler. * * The returned form is an independent Crawler created to take care * of the following issues currently experienced by Crawler's form * object: * - input fields disabled at a higher level (e.g. by a surrounding * fieldset) still return values * - Codeception expects an empty value to match an unselected * select box. * * The function clones the crawler's node and creates a new crawler * because it destroys or adds to the DOM for the form to achieve * the desired functionality. Other functions simply querying the * DOM wouldn't expect them. * * @param Crawler $form the form * @return Form */ private function getFormFromCrawler(Crawler $form) { $fakeDom = new \DOMDocument(); $fakeDom->appendChild($fakeDom->importNode($form->getNode(0), true)); $node = $fakeDom->documentElement; $action = (string)$this->getFormUrl($form); $cloned = new Crawler($node, $action, $this->getBaseUrl()); $shouldDisable = $cloned->filter( 'input:disabled:not([disabled]),select option:disabled,select optgroup:disabled option:not([disabled]),textarea:disabled:not([disabled]),select:disabled:not([disabled])' ); foreach ($shouldDisable as $field) { $field->parentNode->removeChild($field); } return $cloned->form(); } /** * Returns the DomCrawler\Form object for the form pointed to by * $node or its closes form parent. * * @param \Symfony\Component\DomCrawler\Crawler $node * @return \Symfony\Component\DomCrawler\Form */ protected function getFormFor(Crawler $node) { if (strcasecmp($node->first()->getNode(0)->tagName, 'form') === 0) { $form = $node->first(); } else { $form = $node->parents()->filter('form')->first(); } if (!$form) { $this->fail('The selected node is not a form and does not have a form ancestor.'); } $identifier = $form->attr('id') ?: $form->attr('action'); if (!isset($this->forms[$identifier])) { $this->forms[$identifier] = $this->getFormFromCrawler($form); } return $this->forms[$identifier]; } /** * Returns an array of name => value pairs for the passed form. * * For form fields containing a name ending in [], an array is * created out of all field values with the given name. * * @param \Symfony\Component\DomCrawler\Form the form * @return array an array of name => value pairs */ protected function getFormValuesFor(Form $form) { $values = []; $fields = $form->all(); foreach ($fields as $field) { if ($field->isDisabled() || !$field->hasValue() || $field instanceof FileFormField) { continue; } $fieldName = $this->getSubmissionFormFieldName($field->getName()); if (substr($field->getName(), -2) === '[]') { if (!isset($values[$fieldName])) { $values[$fieldName] = []; } $values[$fieldName][] = $field->getValue(); } else { $values[$fieldName] = $field->getValue(); } } return $values; } public function fillField($field, $value) { $input = $this->getFieldByLabelOrCss($field); $form = $this->getFormFor($input); $name = $input->attr('name'); $dynamicField = $input->getNode(0)->tagName == 'textarea' ? new TextareaFormField($input->getNode(0)) : new InputFormField($input->getNode(0)); $formField = $this->matchFormField($name, $form, $dynamicField); $formField->setValue($value); $input->getNode(0)->setAttribute('value', htmlspecialchars($value)); if ($input->getNode(0)->tagName == 'textarea') { $input->getNode(0)->nodeValue = htmlspecialchars($value); } } /** * @param $field * * @return \Symfony\Component\DomCrawler\Crawler */ protected function getFieldsByLabelOrCss($field) { if (is_array($field)) { $input = $this->strictMatch($field); if (!count($input)) { throw new ElementNotFound($field); } return $input; } // by label $label = $this->strictMatch(['xpath' => sprintf('.//label[descendant-or-self::node()[text()[normalize-space()=%s]]]', Crawler::xpathLiteral($field))]); if (count($label)) { $label = $label->first(); if ($label->attr('for')) { $input = $this->strictMatch(['id' => $label->attr('for')]); } else { $input = $this->strictMatch(['xpath' => sprintf('.//label[descendant-or-self::node()[text()[normalize-space()=%s]]]//input', Crawler::xpathLiteral($field))]); } } // by name if (!isset($input)) { $input = $this->strictMatch(['name' => $field]); } // by CSS and XPath if (!count($input)) { $input = $this->match($field); } if (!count($input)) { throw new ElementNotFound($field, 'Form field by Label or CSS'); } return $input; } protected function getFieldByLabelOrCss($field) { $input = $this->getFieldsByLabelOrCss($field); return $input->first(); } public function selectOption($select, $option) { $field = $this->getFieldByLabelOrCss($select); $form = $this->getFormFor($field); $fieldName = $this->getSubmissionFormFieldName($field->attr('name')); if (is_array($option)) { if (!isset($option[0])) { // strict option locator $form[$fieldName]->select($this->matchOption($field, $option)); codecept_debug($option); return; } $options = []; foreach ($option as $opt) { $options[] = $this->matchOption($field, $opt); } $form[$fieldName]->select($options); return; } $dynamicField = new ChoiceFormField($field->getNode(0)); $formField = $this->matchFormField($fieldName, $form, $dynamicField); $selValue = $this->matchOption($field, $option); if (is_array($formField)) { foreach ($formField as $field) { $values = $field->availableOptionValues(); foreach ($values as $val) { if ($val === $option) { $field->select($selValue); return; } } } return; } $formField->select($this->matchOption($field, $option)); } protected function matchOption(Crawler $field, $option) { if (isset($option['value'])) { return $option['value']; } if (isset($option['text'])) { $option = $option['text']; } $options = $field->filterXPath(sprintf('//option[text()=normalize-space("%s")]|//input[@type="radio" and @value=normalize-space("%s")]', $option, $option)); if ($options->count()) { $firstMatchingDomNode = $options->getNode(0); if ($firstMatchingDomNode->tagName === 'option') { $firstMatchingDomNode->setAttribute('selected', 'selected'); } else { $firstMatchingDomNode->setAttribute('checked', 'checked'); } $valueAttribute = $options->first()->attr('value'); //attr() returns null when option has no value attribute if ($valueAttribute !== null) { return $valueAttribute; } return $options->first()->text(); } return $option; } public function checkOption($option) { $this->proceedCheckOption($option)->tick(); } public function uncheckOption($option) { $this->proceedCheckOption($option)->untick(); } /** * @param $option * @return ChoiceFormField */ protected function proceedCheckOption($option) { $form = $this->getFormFor($field = $this->getFieldByLabelOrCss($option)); $name = $field->attr('name'); if ($field->getNode(0) === null) { throw new TestRuntimeException("Form field $name is not located"); } // If the name is an array than we compare objects to find right checkbox $formField = $this->matchFormField($name, $form, new ChoiceFormField($field->getNode(0))); $field->getNode(0)->setAttribute('checked', 'checked'); if (!$formField instanceof ChoiceFormField) { throw new TestRuntimeException("Form field $name is not a checkable"); } return $formField; } public function attachFile($field, $filename) { $form = $this->getFormFor($field = $this->getFieldByLabelOrCss($field)); $filePath = codecept_data_dir() . $filename; if (!file_exists($filePath)) { throw new \InvalidArgumentException("File does not exist: $filePath"); } if (!is_readable($filePath)) { throw new \InvalidArgumentException("File is not readable: $filePath"); } $name = $field->attr('name'); $formField = $this->matchFormField($name, $form, new FileFormField($field->getNode(0))); if (is_array($formField)) { $this->fail("Field $name is ignored on upload, field $name is treated as array."); } $formField->upload($filePath); } /** * If your page triggers an ajax request, you can perform it manually. * This action sends a GET ajax request with specified params. * * See ->sendAjaxPostRequest for examples. * * @param $uri * @param $params */ public function sendAjaxGetRequest($uri, $params = []) { $this->sendAjaxRequest('GET', $uri, $params); } /** * If your page triggers an ajax request, you can perform it manually. * This action sends a POST ajax request with specified params. * Additional params can be passed as array. * * Example: * * Imagine that by clicking checkbox you trigger ajax request which updates user settings. * We emulate that click by running this ajax request manually. * * ``` php * sendAjaxPostRequest('/updateSettings', array('notifications' => true)); // POST * $I->sendAjaxGetRequest('/updateSettings', array('notifications' => true)); // GET * * ``` * * @param $uri * @param $params */ public function sendAjaxPostRequest($uri, $params = []) { $this->sendAjaxRequest('POST', $uri, $params); } /** * If your page triggers an ajax request, you can perform it manually. * This action sends an ajax request with specified method and params. * * Example: * * You need to perform an ajax request specifying the HTTP method. * * ``` php * sendAjaxRequest('PUT', '/posts/7', array('title' => 'new title')); * * ``` * * @param $method * @param $uri * @param $params */ public function sendAjaxRequest($method, $uri, $params = []) { $this->clientRequest($method, $uri, $params, [], ['HTTP_X_REQUESTED_WITH' => 'XMLHttpRequest'], null, false); } /** * @param $url */ protected function debugResponse($url) { $this->debugSection('Page', $url); $this->debugSection('Response', $this->getResponseStatusCode()); $this->debugSection('Request Cookies', $this->getRunningClient()->getInternalRequest()->getCookies()); $this->debugSection('Response Headers', $this->getRunningClient()->getInternalResponse()->getHeaders()); } public function makeHtmlSnapshot($name = null) { if (empty($name)) { $name = uniqid(date("Y-m-d_H-i-s_")); } $debugDir = codecept_output_dir() . 'debug'; if (!is_dir($debugDir)) { mkdir($debugDir, 0777); } $fileName = $debugDir . DIRECTORY_SEPARATOR . $name . '.html'; $this->_savePageSource($fileName); $this->debugSection('Snapshot Saved', "file://$fileName"); } public function _getResponseStatusCode() { return $this->getResponseStatusCode(); } protected function getResponseStatusCode() { // depending on Symfony version $response = $this->getRunningClient()->getInternalResponse(); if (method_exists($response, 'getStatusCode')) { return $response->getStatusCode(); } if (method_exists($response, 'getStatus')) { return $response->getStatus(); } return "N/A"; } /** * @param $selector * * @return Crawler */ protected function match($selector) { if (is_array($selector)) { return $this->strictMatch($selector); } if (Locator::isCSS($selector)) { return $this->getCrawler()->filter($selector); } if (Locator::isXPath($selector)) { return $this->getCrawler()->filterXPath($selector); } throw new MalformedLocatorException($selector, 'XPath or CSS'); } /** * @param array $by * @throws TestRuntimeException * @return Crawler */ protected function strictMatch(array $by) { $type = key($by); $locator = $by[$type]; switch ($type) { case 'id': return $this->filterByCSS("#$locator"); case 'name': return $this->filterByXPath(sprintf('.//*[@name=%s]', Crawler::xpathLiteral($locator))); case 'css': return $this->filterByCSS($locator); case 'xpath': return $this->filterByXPath($locator); case 'link': return $this->filterByXPath(sprintf('.//a[.=%s or contains(./@title, %s)]', Crawler::xpathLiteral($locator), Crawler::xpathLiteral($locator))); case 'class': return $this->filterByCSS(".$locator"); default: throw new TestRuntimeException( "Locator type '$by' is not defined. Use either: xpath, css, id, link, class, name" ); } } protected function filterByAttributes(Crawler $nodes, array $attributes) { foreach ($attributes as $attr => $val) { $nodes = $nodes->reduce( function (Crawler $node) use ($attr, $val) { return $node->attr($attr) == $val; } ); } return $nodes; } public function grabTextFrom($cssOrXPathOrRegex) { if (@preg_match($cssOrXPathOrRegex, $this->client->getInternalResponse()->getContent(), $matches)) { return $matches[1]; } $nodes = $this->match($cssOrXPathOrRegex); if ($nodes->count()) { return $nodes->first()->text(); } throw new ElementNotFound($cssOrXPathOrRegex, 'Element that matches CSS or XPath or Regex'); } public function grabAttributeFrom($cssOrXpath, $attribute) { $nodes = $this->match($cssOrXpath); if (!$nodes->count()) { throw new ElementNotFound($cssOrXpath, 'Element that matches CSS or XPath'); } return $nodes->first()->attr($attribute); } public function grabMultiple($cssOrXpath, $attribute = null) { $result = []; $nodes = $this->match($cssOrXpath); foreach ($nodes as $node) { if ($attribute !== null) { $result[] = $node->getAttribute($attribute); } else { $result[] = $node->textContent; } } return $result; } /** * @param $field * * @return array|mixed|null|string */ public function grabValueFrom($field) { $nodes = $this->match($field); if (!$nodes->count()) { throw new ElementNotFound($field, 'Field'); } if ($nodes->filter('textarea')->count()) { return (new TextareaFormField($nodes->filter('textarea')->getNode(0)))->getValue(); } $input = $nodes->filter('input'); if ($input->count()) { return $this->getInputValue($input); } if ($nodes->filter('select')->count()) { $field = new ChoiceFormField($nodes->filter('select')->getNode(0)); $options = $nodes->filter('option[selected]'); $values = []; foreach ($options as $option) { $values[] = $option->getAttribute('value'); } if (!$field->isMultiple()) { return reset($values); } return $values; } $this->fail("Element $nodes is not a form field or does not contain a form field"); } public function setCookie($name, $val, array $params = []) { $cookies = $this->client->getCookieJar(); $params = array_merge($this->defaultCookieParameters, $params); $expires = isset($params['expiry']) ? $params['expiry'] : null; // WebDriver compatibility $expires = isset($params['expires']) && !$expires ? $params['expires'] : null; $path = isset($params['path']) ? $params['path'] : null; $domain = isset($params['domain']) ? $params['domain'] : ''; $secure = isset($params['secure']) ? $params['secure'] : false; $httpOnly = isset($params['httpOnly']) ? $params['httpOnly'] : true; $encodedValue = isset($params['encodedValue']) ? $params['encodedValue'] : false; $cookies->set(new Cookie($name, $val, $expires, $path, $domain, $secure, $httpOnly, $encodedValue)); $this->debugCookieJar(); } public function grabCookie($cookie, array $params = []) { $params = array_merge($this->defaultCookieParameters, $params); $this->debugCookieJar(); $cookies = $this->getRunningClient()->getCookieJar()->get($cookie, $params['path'], $params['domain']); if (!$cookies) { return null; } return $cookies->getValue(); } /** * Grabs current page source code. * * @throws ModuleException if no page was opened. * * @return string Current page source code. */ public function grabPageSource() { return $this->_getResponseContent(); } public function seeCookie($cookie, array $params = []) { $params = array_merge($this->defaultCookieParameters, $params); $this->debugCookieJar(); $this->assertNotNull($this->client->getCookieJar()->get($cookie, $params['path'], $params['domain'])); } public function dontSeeCookie($cookie, array $params = []) { $params = array_merge($this->defaultCookieParameters, $params); $this->debugCookieJar(); $this->assertNull($this->client->getCookieJar()->get($cookie, $params['path'], $params['domain'])); } public function resetCookie($name, array $params = []) { $params = array_merge($this->defaultCookieParameters, $params); $this->client->getCookieJar()->expire($name, $params['path'], $params['domain']); $this->debugCookieJar(); } private function stringifySelector($selector) { if (is_array($selector)) { return trim(json_encode($selector), '{}'); } return $selector; } public function seeElement($selector, $attributes = []) { $nodes = $this->match($selector); $selector = $this->stringifySelector($selector); if (!empty($attributes)) { $nodes = $this->filterByAttributes($nodes, $attributes); $selector .= "' with attribute(s) '" . trim(json_encode($attributes), '{}'); } $this->assertDomContains($nodes, $selector); } public function dontSeeElement($selector, $attributes = []) { $nodes = $this->match($selector); $selector = $this->stringifySelector($selector); if (!empty($attributes)) { $nodes = $this->filterByAttributes($nodes, $attributes); $selector .= "' with attribute(s) '" . trim(json_encode($attributes), '{}'); } $this->assertDomNotContains($nodes, $selector); } public function seeNumberOfElements($selector, $expected) { $counted = count($this->match($selector)); if (is_array($expected)) { list($floor, $ceil) = $expected; $this->assertTrue( $floor <= $counted && $ceil >= $counted, 'Number of elements counted differs from expected range' ); } else { $this->assertEquals( $expected, $counted, 'Number of elements counted differs from expected number' ); } } public function seeOptionIsSelected($selector, $optionText) { $selected = $this->matchSelectedOption($selector); $this->assertDomContains($selected, 'selected option'); //If element is radio then we need to check value $value = $selected->getNode(0)->tagName == 'option' ? $selected->text() : $selected->getNode(0)->getAttribute('value'); $this->assertEquals($optionText, $value); } public function dontSeeOptionIsSelected($selector, $optionText) { $selected = $this->matchSelectedOption($selector); if (!$selected->count()) { $this->assertEquals(0, $selected->count()); return; } //If element is radio then we need to check value $value = $selected->getNode(0)->tagName == 'option' ? $selected->text() : $selected->getNode(0)->getAttribute('value'); $this->assertNotEquals($optionText, $value); } protected function matchSelectedOption($select) { $nodes = $this->getFieldsByLabelOrCss($select); $selectedOptions = $nodes->filter('option[selected],input:checked'); if ($selectedOptions->count() == 0) { $selectedOptions = $nodes->filter('option,input')->first(); } return $selectedOptions; } /** * Asserts that current page has 404 response status code. */ public function seePageNotFound() { $this->seeResponseCodeIs(404); } /** * Checks that response code is equal to value provided. * * ```php * seeResponseCodeIs(200); * * // recommended \Codeception\Util\HttpCode * $I->seeResponseCodeIs(\Codeception\Util\HttpCode::OK); * ``` * * @param $code */ public function seeResponseCodeIs($code) { $failureMessage = sprintf( 'Expected HTTP Status Code: %s. Actual Status Code: %s', HttpCode::getDescription($code), HttpCode::getDescription($this->getResponseStatusCode()) ); $this->assertEquals($code, $this->getResponseStatusCode(), $failureMessage); } /** * Checks that response code is between a certain range. Between actually means [from <= CODE <= to] * * @param $from * @param $to */ public function seeResponseCodeIsBetween($from, $to) { $failureMessage = sprintf( 'Expected HTTP Status Code between %s and %s. Actual Status Code: %s', HttpCode::getDescription($from), HttpCode::getDescription($to), HttpCode::getDescription($this->getResponseStatusCode()) ); $this->assertGreaterThanOrEqual($from, $this->getResponseStatusCode(), $failureMessage); $this->assertLessThanOrEqual($to, $this->getResponseStatusCode(), $failureMessage); } /** * Checks that response code is equal to value provided. * * ```php * dontSeeResponseCodeIs(200); * * // recommended \Codeception\Util\HttpCode * $I->dontSeeResponseCodeIs(\Codeception\Util\HttpCode::OK); * ``` * @param $code */ public function dontSeeResponseCodeIs($code) { $failureMessage = sprintf( 'Expected HTTP status code other than %s', HttpCode::getDescription($code) ); $this->assertNotEquals($code, $this->getResponseStatusCode(), $failureMessage); } /** * Checks that the response code 2xx */ public function seeResponseCodeIsSuccessful() { $this->seeResponseCodeIsBetween(200, 299); } /** * Checks that the response code 3xx */ public function seeResponseCodeIsRedirection() { $this->seeResponseCodeIsBetween(300, 399); } /** * Checks that the response code is 4xx */ public function seeResponseCodeIsClientError() { $this->seeResponseCodeIsBetween(400, 499); } /** * Checks that the response code is 5xx */ public function seeResponseCodeIsServerError() { $this->seeResponseCodeIsBetween(500, 599); } public function seeInTitle($title) { $nodes = $this->getCrawler()->filter('title'); if (!$nodes->count()) { throw new ElementNotFound("", "Tag"); } $this->assertStringContainsString($title, $nodes->first()->text(), "page title contains $title"); } public function dontSeeInTitle($title) { $nodes = $this->getCrawler()->filter('title'); if (!$nodes->count()) { $this->assertTrue(true); return; } $this->assertStringNotContainsString($title, $nodes->first()->text(), "page title contains $title"); } protected function assertDomContains($nodes, $message, $text = '') { $constraint = new CrawlerConstraint($text, $this->_getCurrentUri()); $this->assertThat($nodes, $constraint, $message); } protected function assertDomNotContains($nodes, $message, $text = '') { $constraint = new CrawlerNotConstraint($text, $this->_getCurrentUri()); $this->assertThat($nodes, $constraint, $message); } protected function assertPageContains($needle, $message = '') { $constraint = new PageConstraint($needle, $this->_getCurrentUri()); $this->assertThat( $this->getNormalizedResponseContent(), $constraint, $message ); } protected function assertPageNotContains($needle, $message = '') { $constraint = new PageConstraint($needle, $this->_getCurrentUri()); $this->assertThatItsNot( $this->getNormalizedResponseContent(), $constraint, $message ); } protected function assertPageSourceContains($needle, $message = '') { $constraint = new PageConstraint($needle, $this->_getCurrentUri()); $this->assertThat( $this->_getResponseContent(), $constraint, $message ); } protected function assertPageSourceNotContains($needle, $message = '') { $constraint = new PageConstraint($needle, $this->_getCurrentUri()); $this->assertThatItsNot( $this->_getResponseContent(), $constraint, $message ); } /** * @param $name * @param $form * @param $dynamicField * @return FormField */ protected function matchFormField($name, $form, $dynamicField) { if (substr($name, -2) != '[]') { return $form[$name]; } $name = substr($name, 0, -2); /** @var $item \Symfony\Component\DomCrawler\Field\FormField */ foreach ($form[$name] as $item) { if ($item == $dynamicField) { return $item; } } throw new TestRuntimeException("None of form fields by {$name}[] were not matched"); } /** * @param $locator * @return Crawler */ protected function filterByCSS($locator) { if (!Locator::isCSS($locator)) { throw new MalformedLocatorException($locator, 'css'); } return $this->getCrawler()->filter($locator); } /** * @param $locator * @return Crawler */ protected function filterByXPath($locator) { if (!Locator::isXPath($locator)) { throw new MalformedLocatorException($locator, 'xpath'); } return $this->getCrawler()->filterXPath($locator); } /** * @param $requestParams * @return array */ protected function getFormPhpValues($requestParams) { foreach ($requestParams as $name => $value) { $qs = http_build_query([$name => $value], '', '&'); if (!empty($qs)) { // If the field's name is of the form of "array[key]", // we'll remove it from the request parameters // and set the "array" key instead which will contain the actual array. if (strpos($name, '[') && strpos($name, ']') > strpos($name, '[')) { unset($requestParams[$name]); } parse_str($qs, $expandedValue); $varName = substr($name, 0, strlen(key($expandedValue))); $requestParams = array_replace_recursive($requestParams, [$varName => current($expandedValue)]); } } return $requestParams; } /** * @param $result * @return mixed */ protected function redirectIfNecessary($result, $maxRedirects, $redirectCount) { $locationHeader = $this->client->getInternalResponse()->getHeader('Location'); $statusCode = $this->getResponseStatusCode(); if ($locationHeader && $statusCode >= 300 && $statusCode < 400) { if ($redirectCount == $maxRedirects) { throw new \LogicException(sprintf( 'The maximum number (%d) of redirections was reached.', $maxRedirects )); } $this->debugSection('Redirecting to', $locationHeader); $result = $this->client->followRedirect(); $this->debugResponse($locationHeader); return $this->redirectIfNecessary($result, $maxRedirects, $redirectCount + 1); } $this->client->followRedirects(true); return $result; } /** * Switch to iframe or frame on the page. * * Example: * ``` html * <iframe name="another_frame" src="http://example.com"> * ``` * * ``` php * <?php * # switch to iframe * $I->switchToIframe("another_frame"); * ``` * * @param string $name */ public function switchToIframe($name) { $iframe = $this->match("iframe[name=$name]")->first(); if (!count($iframe)) { $iframe = $this->match("frame[name=$name]")->first(); } if (!count($iframe)) { throw new ElementNotFound("name=$name", 'Iframe'); } $uri = $iframe->getNode(0)->getAttribute('src'); $this->amOnPage($uri); } /** * Moves back in history. * * @param int $numberOfSteps (default value 1) */ public function moveBack($numberOfSteps = 1) { if (!is_int($numberOfSteps) || $numberOfSteps < 1) { throw new \InvalidArgumentException('numberOfSteps must be positive integer'); } try { $history = $this->getRunningClient()->getHistory(); for ($i = $numberOfSteps; $i > 0; $i--) { $request = $history->back(); } } catch (\LogicException $e) { throw new \InvalidArgumentException( sprintf( 'numberOfSteps is set to %d, but there are only %d previous steps in the history', $numberOfSteps, $numberOfSteps - $i ) ); } $this->_loadPage( $request->getMethod(), $request->getUri(), $request->getParameters(), $request->getFiles(), $request->getServer(), $request->getContent() ); } protected function debugCookieJar() { $cookies = $this->client->getCookieJar()->all(); $cookieStrings = array_map('strval', $cookies); $this->debugSection('Cookie Jar', $cookieStrings); } protected function setCookiesFromOptions() { if (isset($this->config['cookies']) && is_array($this->config['cookies']) && !empty($this->config['cookies'])) { $domain = parse_url($this->config['url'], PHP_URL_HOST); $cookieJar = $this->client->getCookieJar(); foreach ($this->config['cookies'] as &$cookie) { if (!is_array($cookie) || !array_key_exists('Name', $cookie) || !array_key_exists('Value', $cookie)) { throw new \InvalidArgumentException('Cookies must have at least Name and Value attributes'); } if (!isset($cookie['Domain'])) { $cookie['Domain'] = $domain; } if (!isset($cookie['Expires'])) { $cookie['Expires'] = null; } if (!isset($cookie['Path'])) { $cookie['Path'] = '/'; } if (!isset($cookie['Secure'])) { $cookie['Secure'] = false; } if (!isset($cookie['HttpOnly'])) { $cookie['HttpOnly'] = false; } $cookieJar->set(new \Symfony\Component\BrowserKit\Cookie( $cookie['Name'], $cookie['Value'], $cookie['Expires'], $cookie['Path'], $cookie['Domain'], $cookie['Secure'], $cookie['HttpOnly'] )); } } } /** * @return string */ protected function getNormalizedResponseContent() { $content = $this->_getResponseContent(); // Since strip_tags has problems with JS code that contains // an <= operator the script tags have to be removed manually first. $content = preg_replace('#<script(.*?)>(.*?)</script>#is', '', $content); $content = strip_tags($content); $content = html_entity_decode($content, ENT_QUOTES); $content = str_replace("\n", ' ', $content); $content = preg_replace('/\s{2,}/', ' ', $content); return $content; } } <?php namespace Codeception\Lib\Interfaces; interface ActiveRecord extends ORM { public function haveRecord($model, $attributes = []); public function seeRecord($model, $attributes = []); public function dontSeeRecord($model, $attributes = []); public function grabRecord($model, $attributes = []); } <?php namespace Codeception\Lib\Interfaces; /** * Modules for API testing */ interface API { } <?php namespace Codeception\Lib\Interfaces; interface ConflictsWithModule { /** * Returns class name or interface of module which can conflict with current. * @return string */ public function _conflicts(); } <?php namespace Codeception\Lib\Interfaces; interface DataMapper extends ORM, DoctrineProvider { public function haveInRepository($entity, array $data); public function seeInRepository($entity, $params = []); public function dontSeeInRepository($entity, $params = []); public function grabFromRepository($entity, $field, $params = []); } <?php namespace Codeception\Lib\Interfaces; interface Db { /** * Asserts that a row with the given column values exists. * Provide table name and column values. * * ```php * <?php * $I->seeInDatabase('users', ['name' => 'Davert', 'email' => 'davert@mail.com']); * ``` * Fails if no such user found. * * Comparison expressions can be used as well: * * ```php * <?php * $I->seeInDatabase('posts', ['num_comments >=' => '0']); * $I->seeInDatabase('users', ['email like' => 'miles@davis.com']); * ``` * * Supported operators: `<`, `>`, `>=`, `<=`, `!=`, `like`. * * * @param string $table * @param array $criteria */ public function seeInDatabase($table, $criteria = []); /** * Effect is opposite to ->seeInDatabase * * Asserts that there is no record with the given column values in a database. * Provide table name and column values. * * ``` php * <?php * $I->dontSeeInDatabase('users', ['name' => 'Davert', 'email' => 'davert@mail.com']); * ``` * Fails if such user was found. * * Comparison expressions can be used as well: * * ```php * <?php * $I->dontSeeInDatabase('posts', ['num_comments >=' => '0']); * $I->dontSeeInDatabase('users', ['email like' => 'miles%']); * ``` * * Supported operators: `<`, `>`, `>=`, `<=`, `!=`, `like`. * * @param string $table * @param array $criteria */ public function dontSeeInDatabase($table, $criteria = []); /** * Fetches a single column value from a database. * Provide table name, desired column and criteria. * * ``` php * <?php * $mail = $I->grabFromDatabase('users', 'email', array('name' => 'Davert')); * ``` * Comparison expressions can be used as well: * * ```php * <?php * $post = $I->grabFromDatabase('posts', ['num_comments >=' => 100]); * $user = $I->grabFromDatabase('users', ['email like' => 'miles%']); * ``` * * Supported operators: `<`, `>`, `>=`, `<=`, `!=`, `like`. * * @param string $table * @param string $column * @param array $criteria * * @return mixed */ public function grabFromDatabase($table, $column, $criteria = []); } <?php namespace Codeception\Lib\Interfaces; interface DependsOnModule { /** * Specifies class or module which is required for current one. * * THis method should return array with key as class name and value as error message * [className => errorMessage * ] * @return mixed */ public function _depends(); } <?php namespace Codeception\Lib\Interfaces; interface DoctrineProvider { public function _getEntityManager(); } <?php namespace Codeception\Lib\Interfaces; interface ElementLocator { /** * Locates element using available Codeception locator types: * * * XPath * * CSS * * Strict Locator * * Use it in Helpers or GroupObject or Extension classes: * * ```php * <?php * $els = $this->getModule('{{MODULE_NAME}}')->_findElements('.items'); * $els = $this->getModule('{{MODULE_NAME}}')->_findElements(['name' => 'username']); * * $editLinks = $this->getModule('{{MODULE_NAME}}')->_findElements(['link' => 'Edit']); * // now you can iterate over $editLinks and check that all them have valid hrefs * ``` * * WebDriver module returns `Facebook\WebDriver\Remote\RemoteWebElement` instances * PhpBrowser and Framework modules return `Symfony\Component\DomCrawler\Crawler` instances * * @api * @param $locator * @return array of interactive elements */ public function _findElements($locator); } <?php namespace Codeception\Lib\Interfaces; interface MultiSession { public function _initializeSession(); public function _loadSession($session); public function _backupSession(); public function _closeSession($session = null); public function _getName(); } <?php namespace Codeception\Lib\Interfaces; interface ORM { } <?php namespace Codeception\Lib\Interfaces; interface PageSourceSaver { /** * Saves page source of to a file * * ```php * $this->getModule('{{MODULE_NAME}}')->_savePageSource(codecept_output_dir().'page.html'); * ``` * @api * @param $filename */ public function _savePageSource($filename); /** * Saves current page's HTML into a temprary file. * Use this method in debug mode within an interactive pause to get a source code of current page. * * ```php * <?php * $I->makePageSnapshot('edit_page'); * // saved to: tests/_output/debug/edit_page.html * $I->makePageSnapshot(); * // saved to: tests/_output/debug/2017-05-26_14-24-11_4b3403665fea6.html * ``` * * @param null $name */ public function makeHtmlSnapshot($name = null); } <?php namespace Codeception\Lib\Interfaces; /** * Interface PartedModule * * Module implementing this interface can be loaded partly. * Parts can be defined by marking methods with `@part` annotations. * Part of modules can be loaded by specifying part (or several parts) in config: * * ``` * modules: * enabled: [MyModule] * config: * MyModule: * part: usefulActions * ``` * * * @package Codeception\Lib\Interfaces */ interface PartedModule { public function _parts(); } <?php namespace Codeception\Lib\Interfaces; interface Queue { /** * Connect to the queueing server. * @param array $config * @return */ public function openConnection($config); /** * Post/Put a message on to the queue server * * @param string $message Message Body to be send * @param string $queue Queue Name */ public function addMessageToQueue($message, $queue); /** * Return a list of queues/tubes on the queueing server * * @return array Array of Queues */ public function getQueues(); /** * Count the current number of messages on the queue. * * @param $queue Queue Name * * @return int Count */ public function getMessagesCurrentCountOnQueue($queue); /** * Count the total number of messages on the queue. * * @param $queue Queue Name * * @return int Count */ public function getMessagesTotalCountOnQueue($queue); public function clearQueue($queue); public function getRequiredConfig(); public function getDefaultConfig(); } <?php namespace Codeception\Lib\Interfaces; interface Remote { /** * Changes the subdomain for the 'url' configuration parameter. * Does not open a page; use `amOnPage` for that. * * ``` php * <?php * // If config is: 'http://mysite.com' * // or config is: 'http://www.mysite.com' * // or config is: 'http://company.mysite.com' * * $I->amOnSubdomain('user'); * $I->amOnPage('/'); * // moves to http://user.mysite.com/ * ?> * ``` * * @param $subdomain * * @return mixed */ public function amOnSubdomain($subdomain); /** * Open web page at the given absolute URL and sets its hostname as the base host. * * ``` php * <?php * $I->amOnUrl('http://codeception.com'); * $I->amOnPage('/quickstart'); // moves to http://codeception.com/quickstart * ?> * ``` */ public function amOnUrl($url); public function _getUrl(); } <?php namespace Codeception\Lib\Interfaces; interface RequiresPackage { /** * Returns list of classes and corresponding packages required for this module */ public function _requires(); } <?php namespace Codeception\Lib\Interfaces; interface ScreenshotSaver { /** * Saves screenshot of current page to a file * * ```php * $this->getModule('{{MODULE_NAME}}')->_saveScreenshot(codecept_output_dir().'screenshot_1.png'); * ``` * @api * @param $filename */ public function _saveScreenshot($filename); } <?php namespace Codeception\Lib\Interfaces; interface SessionSnapshot { /** * Saves current cookies into named snapshot in order to restore them in other tests * This is useful to save session state between tests. * For example, if user needs log in to site for each test this scenario can be executed once * while other tests can just restore saved cookies. * * ``` php * <?php * // inside AcceptanceTester class: * * public function login() * { * // if snapshot exists - skipping login * if ($I->loadSessionSnapshot('login')) return; * * // logging in * $I->amOnPage('/login'); * $I->fillField('name', 'jon'); * $I->fillField('password', '123345'); * $I->click('Login'); * * // saving snapshot * $I->saveSessionSnapshot('login'); * } * ?> * ``` * * @param $name * @return mixed */ public function saveSessionSnapshot($name); /** * Loads cookies from a saved snapshot. * Allows to reuse same session across tests without additional login. * * See [saveSessionSnapshot](#saveSessionSnapshot) * * @param $name * @return mixed */ public function loadSessionSnapshot($name); /** * Deletes session snapshot. * * See [saveSessionSnapshot](#saveSessionSnapshot) * * @param $name * @return mixed */ public function deleteSessionSnapshot($name); } <?php namespace Codeception\Lib\Interfaces; interface Web { /** * Opens the page for the given relative URI. * * ``` php * <?php * // opens front page * $I->amOnPage('/'); * // opens /register page * $I->amOnPage('/register'); * ``` * * @param string $page */ public function amOnPage($page); /** * Checks that the current page contains the given string (case insensitive). * * You can specify a specific HTML element (via CSS or XPath) as the second * parameter to only search within that element. * * ``` php * <?php * $I->see('Logout'); // I can suppose user is logged in * $I->see('Sign Up', 'h1'); // I can suppose it's a signup page * $I->see('Sign Up', '//body/h1'); // with XPath * $I->see('Sign Up', ['css' => 'body h1']); // with strict CSS locator * ``` * * Note that the search is done after stripping all HTML tags from the body, * so `$I->see('strong')` will return true for strings like: * * - `<p>I am Stronger than thou</p>` * - `<script>document.createElement('strong');</script>` * * But will *not* be true for strings like: * * - `<strong>Home</strong>` * - `<div class="strong">Home</strong>` * - `<!-- strong -->` * * For checking the raw source code, use `seeInSource()`. * * @param string $text * @param array|string $selector optional */ public function see($text, $selector = null); /** * Checks that the current page doesn't contain the text specified (case insensitive). * Give a locator as the second parameter to match a specific region. * * ```php * <?php * $I->dontSee('Login'); // I can suppose user is already logged in * $I->dontSee('Sign Up','h1'); // I can suppose it's not a signup page * $I->dontSee('Sign Up','//body/h1'); // with XPath * $I->dontSee('Sign Up', ['css' => 'body h1']); // with strict CSS locator * ``` * * Note that the search is done after stripping all HTML tags from the body, * so `$I->dontSee('strong')` will fail on strings like: * * - `<p>I am Stronger than thou</p>` * - `<script>document.createElement('strong');</script>` * * But will ignore strings like: * * - `<strong>Home</strong>` * - `<div class="strong">Home</strong>` * - `<!-- strong -->` * * For checking the raw source code, use `seeInSource()`. * * @param string $text * @param array|string $selector optional */ public function dontSee($text, $selector = null); /** * Checks that the current page contains the given string in its * raw source code. * * ``` php * <?php * $I->seeInSource('<h1>Green eggs & ham</h1>'); * ``` * * @param $raw */ public function seeInSource($raw); /** * Checks that the current page contains the given string in its * raw source code. * * ```php * <?php * $I->dontSeeInSource('<h1>Green eggs & ham</h1>'); * ``` * * @param $raw */ public function dontSeeInSource($raw); /** * Submits the given form on the page, with the given form * values. Pass the form field's values as an array in the second * parameter. * * Although this function can be used as a short-hand version of * `fillField()`, `selectOption()`, `click()` etc. it has some important * differences: * * * Only field *names* may be used, not CSS/XPath selectors nor field labels * * If a field is sent to this function that does *not* exist on the page, * it will silently be added to the HTTP request. This is helpful for testing * some types of forms, but be aware that you will *not* get an exception * like you would if you called `fillField()` or `selectOption()` with * a missing field. * * Fields that are not provided will be filled by their values from the page, * or from any previous calls to `fillField()`, `selectOption()` etc. * You don't need to click the 'Submit' button afterwards. * This command itself triggers the request to form's action. * * You can optionally specify which button's value to include * in the request with the last parameter (as an alternative to * explicitly setting its value in the second parameter), as * button values are not otherwise included in the request. * * Examples: * * ``` php * <?php * $I->submitForm('#login', [ * 'login' => 'davert', * 'password' => '123456' * ]); * // or * $I->submitForm('#login', [ * 'login' => 'davert', * 'password' => '123456' * ], 'submitButtonName'); * * ``` * * For example, given this sample "Sign Up" form: * * ``` html * <form action="/sign_up"> * Login: * <input type="text" name="user[login]" /><br/> * Password: * <input type="password" name="user[password]" /><br/> * Do you agree to our terms? * <input type="checkbox" name="user[agree]" /><br/> * Select pricing plan: * <select name="plan"> * <option value="1">Free</option> * <option value="2" selected="selected">Paid</option> * </select> * <input type="submit" name="submitButton" value="Submit" /> * </form> * ``` * * You could write the following to submit it: * * ``` php * <?php * $I->submitForm( * '#userForm', * [ * 'user' => [ * 'login' => 'Davert', * 'password' => '123456', * 'agree' => true * ] * ], * 'submitButton' * ); * ``` * Note that "2" will be the submitted value for the "plan" field, as it is * the selected option. * * You can also emulate a JavaScript submission by not specifying any * buttons in the third parameter to submitForm. * * ```php * <?php * $I->submitForm( * '#userForm', * [ * 'user' => [ * 'login' => 'Davert', * 'password' => '123456', * 'agree' => true * ] * ] * ); * ``` * * This function works well when paired with `seeInFormFields()` * for quickly testing CRUD interfaces and form validation logic. * * ``` php * <?php * $form = [ * 'field1' => 'value', * 'field2' => 'another value', * 'checkbox1' => true, * // ... * ]; * $I->submitForm('#my-form', $form, 'submitButton'); * // $I->amOnPage('/path/to/form-page') may be needed * $I->seeInFormFields('#my-form', $form); * ``` * * Parameter values can be set to arrays for multiple input fields * of the same name, or multi-select combo boxes. For checkboxes, * you can use either the string value or boolean `true`/`false` which will * be replaced by the checkbox's value in the DOM. * * ``` php * <?php * $I->submitForm('#my-form', [ * 'field1' => 'value', * 'checkbox' => [ * 'value of first checkbox', * 'value of second checkbox', * ], * 'otherCheckboxes' => [ * true, * false, * false * ], * 'multiselect' => [ * 'first option value', * 'second option value' * ] * ]); * ``` * * Mixing string and boolean values for a checkbox's value is not supported * and may produce unexpected results. * * Field names ending in `[]` must be passed without the trailing square * bracket characters, and must contain an array for its value. This allows * submitting multiple values with the same name, consider: * * ```php * <?php * // This will NOT work correctly * $I->submitForm('#my-form', [ * 'field[]' => 'value', * 'field[]' => 'another value', // 'field[]' is already a defined key * ]); * ``` * * The solution is to pass an array value: * * ```php * <?php * // This way both values are submitted * $I->submitForm('#my-form', [ * 'field' => [ * 'value', * 'another value', * ] * ]); * ``` * * @param $selector * @param $params * @param $button */ public function submitForm($selector, array $params, $button = null); /** * Perform a click on a link or a button, given by a locator. * If a fuzzy locator is given, the page will be searched for a button, link, or image matching the locator string. * For buttons, the "value" attribute, "name" attribute, and inner text are searched. * For links, the link text is searched. * For images, the "alt" attribute and inner text of any parent links are searched. * * The second parameter is a context (CSS or XPath locator) to narrow the search. * * Note that if the locator matches a button of type `submit`, the form will be submitted. * * ``` php * <?php * // simple link * $I->click('Logout'); * // button of form * $I->click('Submit'); * // CSS button * $I->click('#form input[type=submit]'); * // XPath * $I->click('//form/*[@type="submit"]'); * // link in context * $I->click('Logout', '#nav'); * // using strict locator * $I->click(['link' => 'Login']); * ?> * ``` * * @param $link * @param $context */ public function click($link, $context = null); /** * Checks that there's a link with the specified text. * Give a full URL as the second parameter to match links with that exact URL. * * ``` php * <?php * $I->seeLink('Logout'); // matches <a target="_top" href="/newspapers?url=#">Logout</a> * $I->seeLink('Logout','/logout'); // matches <a target="_top" href="/newspapers?url=https://codeception.com/logout">Logout</a> * ?> * ``` * * @param string $text * @param string $url optional */ public function seeLink($text, $url = null); /** * Checks that the page doesn't contain a link with the given string. * If the second parameter is given, only links with a matching "href" attribute will be checked. * * ``` php * <?php * $I->dontSeeLink('Logout'); // I suppose user is not logged in * $I->dontSeeLink('Checkout now', '/store/cart.php'); * ?> * ``` * * @param string $text * @param string $url optional */ public function dontSeeLink($text, $url = null); /** * Checks that current URI contains the given string. * * ``` php * <?php * // to match: /home/dashboard * $I->seeInCurrentUrl('home'); * // to match: /users/1 * $I->seeInCurrentUrl('/users/'); * ?> * ``` * * @param string $uri */ public function seeInCurrentUrl($uri); /** * Checks that the current URL is equal to the given string. * Unlike `seeInCurrentUrl`, this only matches the full URL. * * ``` php * <?php * // to match root url * $I->seeCurrentUrlEquals('/'); * ?> * ``` * * @param string $uri */ public function seeCurrentUrlEquals($uri); /** * Checks that the current URL matches the given regular expression. * * ``` php * <?php * // to match root url * $I->seeCurrentUrlMatches('~^/users/(\d+)~'); * ?> * ``` * * @param string $uri */ public function seeCurrentUrlMatches($uri); /** * Checks that the current URI doesn't contain the given string. * * ``` php * <?php * $I->dontSeeInCurrentUrl('/users/'); * ?> * ``` * * @param string $uri */ public function dontSeeInCurrentUrl($uri); /** * Checks that the current URL doesn't equal the given string. * Unlike `dontSeeInCurrentUrl`, this only matches the full URL. * * ``` php * <?php * // current url is not root * $I->dontSeeCurrentUrlEquals('/'); * ?> * ``` * * @param string $uri */ public function dontSeeCurrentUrlEquals($uri); /** * Checks that current url doesn't match the given regular expression. * * ``` php * <?php * // to match root url * $I->dontSeeCurrentUrlMatches('~^/users/(\d+)~'); * ?> * ``` * * @param string $uri */ public function dontSeeCurrentUrlMatches($uri); /** * Executes the given regular expression against the current URI and returns the first capturing group. * If no parameters are provided, the full URI is returned. * * ``` php * <?php * $user_id = $I->grabFromCurrentUrl('~^/user/(\d+)/~'); * $uri = $I->grabFromCurrentUrl(); * ?> * ``` * * @param string $uri optional * * @return mixed */ public function grabFromCurrentUrl($uri = null); /** * Checks that the specified checkbox is checked. * * ``` php * <?php * $I->seeCheckboxIsChecked('#agree'); // I suppose user agreed to terms * $I->seeCheckboxIsChecked('#signup_form input[type=checkbox]'); // I suppose user agreed to terms, If there is only one checkbox in form. * $I->seeCheckboxIsChecked('//form/input[@type=checkbox and @name=agree]'); * ?> * ``` * * @param $checkbox */ public function seeCheckboxIsChecked($checkbox); /** * Check that the specified checkbox is unchecked. * * ``` php * <?php * $I->dontSeeCheckboxIsChecked('#agree'); // I suppose user didn't agree to terms * $I->seeCheckboxIsChecked('#signup_form input[type=checkbox]'); // I suppose user didn't check the first checkbox in form. * ?> * ``` * * @param $checkbox */ public function dontSeeCheckboxIsChecked($checkbox); /** * Checks that the given input field or textarea *equals* (i.e. not just contains) the given value. * Fields are matched by label text, the "name" attribute, CSS, or XPath. * * ``` php * <?php * $I->seeInField('Body','Type your comment here'); * $I->seeInField('form textarea[name=body]','Type your comment here'); * $I->seeInField('form input[type=hidden]','hidden_value'); * $I->seeInField('#searchform input','Search'); * $I->seeInField('//form/*[@name=search]','Search'); * $I->seeInField(['name' => 'search'], 'Search'); * ?> * ``` * * @param $field * @param $value */ public function seeInField($field, $value); /** * Checks that an input field or textarea doesn't contain the given value. * For fuzzy locators, the field is matched by label text, CSS and XPath. * * ``` php * <?php * $I->dontSeeInField('Body','Type your comment here'); * $I->dontSeeInField('form textarea[name=body]','Type your comment here'); * $I->dontSeeInField('form input[type=hidden]','hidden_value'); * $I->dontSeeInField('#searchform input','Search'); * $I->dontSeeInField('//form/*[@name=search]','Search'); * $I->dontSeeInField(['name' => 'search'], 'Search'); * ?> * ``` * * @param $field * @param $value */ public function dontSeeInField($field, $value); /** * Checks if the array of form parameters (name => value) are set on the form matched with the * passed selector. * * ``` php * <?php * $I->seeInFormFields('form[name=myform]', [ * 'input1' => 'value', * 'input2' => 'other value', * ]); * ?> * ``` * * For multi-select elements, or to check values of multiple elements with the same name, an * array may be passed: * * ``` php * <?php * $I->seeInFormFields('.form-class', [ * 'multiselect' => [ * 'value1', * 'value2', * ], * 'checkbox[]' => [ * 'a checked value', * 'another checked value', * ], * ]); * ?> * ``` * * Additionally, checkbox values can be checked with a boolean. * * ``` php * <?php * $I->seeInFormFields('#form-id', [ * 'checkbox1' => true, // passes if checked * 'checkbox2' => false, // passes if unchecked * ]); * ?> * ``` * * Pair this with submitForm for quick testing magic. * * ``` php * <?php * $form = [ * 'field1' => 'value', * 'field2' => 'another value', * 'checkbox1' => true, * // ... * ]; * $I->submitForm('//form[@id=my-form]', $form, 'submitButton'); * // $I->amOnPage('/path/to/form-page') may be needed * $I->seeInFormFields('//form[@id=my-form]', $form); * ?> * ``` * * @param $formSelector * @param $params */ public function seeInFormFields($formSelector, array $params); /** * Checks if the array of form parameters (name => value) are not set on the form matched with * the passed selector. * * ``` php * <?php * $I->dontSeeInFormFields('form[name=myform]', [ * 'input1' => 'non-existent value', * 'input2' => 'other non-existent value', * ]); * ?> * ``` * * To check that an element hasn't been assigned any one of many values, an array can be passed * as the value: * * ``` php * <?php * $I->dontSeeInFormFields('.form-class', [ * 'fieldName' => [ * 'This value shouldn\'t be set', * 'And this value shouldn\'t be set', * ], * ]); * ?> * ``` * * Additionally, checkbox values can be checked with a boolean. * * ``` php * <?php * $I->dontSeeInFormFields('#form-id', [ * 'checkbox1' => true, // fails if checked * 'checkbox2' => false, // fails if unchecked * ]); * ?> * ``` * * @param $formSelector * @param $params */ public function dontSeeInFormFields($formSelector, array $params); /** * Selects an option in a select tag or in radio button group. * * ``` php * <?php * $I->selectOption('form select[name=account]', 'Premium'); * $I->selectOption('form input[name=payment]', 'Monthly'); * $I->selectOption('//form/select[@name=account]', 'Monthly'); * ?> * ``` * * Provide an array for the second argument to select multiple options: * * ``` php * <?php * $I->selectOption('Which OS do you use?', array('Windows','Linux')); * ?> * ``` * * Or provide an associative array for the second argument to specifically define which selection method should be used: * * ``` php * <?php * $I->selectOption('Which OS do you use?', array('text' => 'Windows')); // Only search by text 'Windows' * $I->selectOption('Which OS do you use?', array('value' => 'windows')); // Only search by value 'windows' * ?> * ``` * * @param $select * @param $option */ public function selectOption($select, $option); /** * Ticks a checkbox. For radio buttons, use the `selectOption` method instead. * * ``` php * <?php * $I->checkOption('#agree'); * ?> * ``` * * @param $option */ public function checkOption($option); /** * Unticks a checkbox. * * ``` php * <?php * $I->uncheckOption('#notify'); * ?> * ``` * * @param $option */ public function uncheckOption($option); /** * Fills a text field or textarea with the given string. * * ``` php * <?php * $I->fillField("//input[@type='text']", "Hello World!"); * $I->fillField(['name' => 'email'], 'jon@mail.com'); * ?> * ``` * * @param $field * @param $value */ public function fillField($field, $value); /** * Attaches a file relative to the Codeception `_data` directory to the given file upload field. * * ``` php * <?php * // file is stored in 'tests/_data/prices.xls' * $I->attachFile('input[@type="file"]', 'prices.xls'); * ?> * ``` * * @param $field * @param $filename */ public function attachFile($field, $filename); /** * Finds and returns the text contents of the given element. * If a fuzzy locator is used, the element is found using CSS, XPath, * and by matching the full page source by regular expression. * * ``` php * <?php * $heading = $I->grabTextFrom('h1'); * $heading = $I->grabTextFrom('descendant-or-self::h1'); * $value = $I->grabTextFrom('~<input value=(.*?)]~sgi'); // match with a regex * ?> * ``` * * @param $cssOrXPathOrRegex * * @return mixed */ public function grabTextFrom($cssOrXPathOrRegex); /** * Finds the value for the given form field. * If a fuzzy locator is used, the field is found by field name, CSS, and XPath. * * ``` php * <?php * $name = $I->grabValueFrom('Name'); * $name = $I->grabValueFrom('input[name=username]'); * $name = $I->grabValueFrom('descendant-or-self::form/descendant::input[@name = 'username']'); * $name = $I->grabValueFrom(['name' => 'username']); * ?> * ``` * * @param $field * * @return mixed */ public function grabValueFrom($field); /** * Grabs the value of the given attribute value from the given element. * Fails if element is not found. * * ``` php * <?php * $I->grabAttributeFrom('#tooltip', 'title'); * ?> * ``` * * * @param $cssOrXpath * @param $attribute * * @return mixed */ public function grabAttributeFrom($cssOrXpath, $attribute); /** * Grabs either the text content, or attribute values, of nodes * matched by $cssOrXpath and returns them as an array. * * ```html * <a target="_top" href="/newspapers?url=#first">First</a> * <a target="_top" href="/newspapers?url=#second">Second</a> * <a target="_top" href="/newspapers?url=#third">Third</a> * ``` * * ```php * <?php * // would return ['First', 'Second', 'Third'] * $aLinkText = $I->grabMultiple('a'); * * // would return ['#first', '#second', '#third'] * $aLinks = $I->grabMultiple('a', 'href'); * ?> * ``` * * @param $cssOrXpath * @param $attribute * @return string[] */ public function grabMultiple($cssOrXpath, $attribute = null); /** * Checks that the given element exists on the page and is visible. * You can also specify expected attributes of this element. * * ``` php * <?php * $I->seeElement('.error'); * $I->seeElement('//form/input[1]'); * $I->seeElement('input', ['name' => 'login']); * $I->seeElement('input', ['value' => '123456']); * * // strict locator in first arg, attributes in second * $I->seeElement(['css' => 'form input'], ['name' => 'login']); * ?> * ``` * * @param $selector * @param array $attributes * @return */ public function seeElement($selector, $attributes = []); /** * Checks that the given element is invisible or not present on the page. * You can also specify expected attributes of this element. * * ``` php * <?php * $I->dontSeeElement('.error'); * $I->dontSeeElement('//form/input[1]'); * $I->dontSeeElement('input', ['name' => 'login']); * $I->dontSeeElement('input', ['value' => '123456']); * ?> * ``` * * @param $selector * @param array $attributes */ public function dontSeeElement($selector, $attributes = []); /** * Checks that there are a certain number of elements matched by the given locator on the page. * * ``` php * <?php * $I->seeNumberOfElements('tr', 10); * $I->seeNumberOfElements('tr', [0,10]); // between 0 and 10 elements * ?> * ``` * @param $selector * @param mixed $expected int or int[] */ public function seeNumberOfElements($selector, $expected); /** * Checks that the given option is selected. * * ``` php * <?php * $I->seeOptionIsSelected('#form input[name=payment]', 'Visa'); * ?> * ``` * * @param $selector * @param $optionText * * @return mixed */ public function seeOptionIsSelected($selector, $optionText); /** * Checks that the given option is not selected. * * ``` php * <?php * $I->dontSeeOptionIsSelected('#form input[name=payment]', 'Visa'); * ?> * ``` * * @param $selector * @param $optionText * * @return mixed */ public function dontSeeOptionIsSelected($selector, $optionText); /** * Checks that the page title contains the given string. * * ``` php * <?php * $I->seeInTitle('Blog - Post #1'); * ?> * ``` * * @param $title * * @return mixed */ public function seeInTitle($title); /** * Checks that the page title does not contain the given string. * * @param $title * * @return mixed */ public function dontSeeInTitle($title); /** * Checks that a cookie with the given name is set. * You can set additional cookie params like `domain`, `path` as array passed in last argument. * * ``` php * <?php * $I->seeCookie('PHPSESSID'); * ?> * ``` * * @param $cookie * @param array $params * @return mixed */ public function seeCookie($cookie, array $params = []); /** * Checks that there isn't a cookie with the given name. * You can set additional cookie params like `domain`, `path` as array passed in last argument. * * @param $cookie * * @param array $params * @return mixed */ public function dontSeeCookie($cookie, array $params = []); /** * Sets a cookie with the given name and value. * You can set additional cookie params like `domain`, `path`, `expires`, `secure` in array passed as last argument. * * ``` php * <?php * $I->setCookie('PHPSESSID', 'el4ukv0kqbvoirg7nkp4dncpk3'); * ?> * ``` * * @param $name * @param $val * @param array $params * * @return mixed */ public function setCookie($name, $val, array $params = []); /** * Unsets cookie with the given name. * You can set additional cookie params like `domain`, `path` in array passed as last argument. * * @param $cookie * * @param array $params * @return mixed */ public function resetCookie($cookie, array $params = []); /** * Grabs a cookie value. * You can set additional cookie params like `domain`, `path` in array passed as last argument. * * @param $cookie * * @param array $params * @return mixed */ public function grabCookie($cookie, array $params = []); /** * Grabs current page source code. * * @return string Current page source code. */ public function grabPageSource(); } <?php namespace Codeception\Lib; use Codeception\Configuration; use Codeception\Exception\ConfigurationException; use Codeception\Exception\ModuleConflictException; use Codeception\Exception\ModuleException; use Codeception\Exception\ModuleRequireException; use Codeception\Lib\Interfaces\ConflictsWithModule; use Codeception\Lib\Interfaces\DependsOnModule; use Codeception\Lib\Interfaces\PartedModule; use Codeception\Util\Annotation; /** * Class ModuleContainer * @package Codeception\Lib */ class ModuleContainer { /** * @var string */ const MODULE_NAMESPACE = '\\Codeception\\Module\\'; /** * @var array */ private $config; /** * @var Di */ private $di; /** * @var array */ private $modules = []; /** * @var array */ private $active = []; /** * @var array */ private $actions = []; /** * Constructor. * * @param Di $di * @param array $config */ public function __construct(Di $di, $config) { $this->di = $di; $this->di->set($this); $this->config = $config; } /** * Create a module. * * @param string $moduleName * @param bool $active * @return \Codeception\Module * @throws \Codeception\Exception\ConfigurationException * @throws \Codeception\Exception\ModuleException * @throws \Codeception\Exception\ModuleRequireException * @throws \Codeception\Exception\InjectionException */ public function create($moduleName, $active = true) { $this->active[$moduleName] = $active; $moduleClass = $this->getModuleClass($moduleName); if (!class_exists($moduleClass)) { throw new ConfigurationException("Module $moduleName could not be found and loaded"); } $config = $this->getModuleConfig($moduleName); if (empty($config) && !$active) { // For modules that are a dependency of other modules we want to skip the validation of the config. // This config validation is performed in \Codeception\Module::__construct(). // Explicitly setting $config to null skips this validation. $config = null; } $this->modules[$moduleName] = $module = $this->di->instantiate($moduleClass, [$this, $config], false); if ($this->moduleHasDependencies($module)) { $this->injectModuleDependencies($moduleName, $module); } // If module is not active its actions should not be included in the actor class $actions = $active ? $this->getActionsForModule($module, $config) : []; foreach ($actions as $action) { $this->actions[$action] = $moduleName; }; return $module; } /** * Does a module have dependencies? * * @param \Codeception\Module $module * @return bool */ private function moduleHasDependencies($module) { if (!$module instanceof DependsOnModule) { return false; } return (bool) $module->_depends(); } /** * Get the actions of a module. * * @param \Codeception\Module $module * @param array $config * @return array */ private function getActionsForModule($module, $config) { $reflectionClass = new \ReflectionClass($module); // Only public methods can be actions $methods = $reflectionClass->getMethods(\ReflectionMethod::IS_PUBLIC); // Should this module be loaded partially? $configuredParts = null; if ($module instanceof PartedModule && isset($config['part'])) { $configuredParts = is_array($config['part']) ? $config['part'] : [$config['part']]; } $actions = []; foreach ($methods as $method) { if ($this->includeMethodAsAction($module, $method, $configuredParts)) { $actions[] = $method->name; } } return $actions; } /** * Should a method be included as an action? * * @param \Codeception\Module $module * @param \ReflectionMethod $method * @param array|null $configuredParts * @return bool */ private function includeMethodAsAction($module, $method, $configuredParts = null) { // Filter out excluded actions if ($module::$excludeActions && in_array($method->name, $module::$excludeActions)) { return false; } // Keep only the $onlyActions if they are specified if ($module::$onlyActions && !in_array($method->name, $module::$onlyActions)) { return false; } // Do not include inherited actions if the static $includeInheritedActions property is set to false. // However, if an inherited action is also specified in the static $onlyActions property // it should be included as an action. if (!$module::$includeInheritedActions && !in_array($method->name, $module::$onlyActions) && $method->getDeclaringClass()->getName() != get_class($module) ) { return false; } // Do not include hidden methods, methods with a name starting with an underscore if (strpos($method->name, '_') === 0) { return false; }; // If a part is configured for the module, only include actions from that part if ($configuredParts) { $moduleParts = Annotation::forMethod($module, $method->name)->fetchAll('part'); if (!array_uintersect($moduleParts, $configuredParts, 'strcasecmp')) { return false; } } return true; } /** * Is the module a helper? * * @param string $moduleName * @return bool */ private function isHelper($moduleName) { return strpos($moduleName, '\\') !== false; } /** * Get the fully qualified class name for a module. * * @param string $moduleName * @return string */ private function getModuleClass($moduleName) { if ($this->isHelper($moduleName)) { return $moduleName; } return self::MODULE_NAMESPACE . $moduleName; } /** * Is a module instantiated in this ModuleContainer? * * @param string $moduleName * @return bool */ public function hasModule($moduleName) { return isset($this->modules[$moduleName]); } /** * Get a module from this ModuleContainer. * * @param string $moduleName * @return \Codeception\Module * @throws \Codeception\Exception\ModuleException */ public function getModule($moduleName) { if (!$this->hasModule($moduleName)) { throw new ModuleException(__CLASS__, "Module $moduleName couldn't be connected"); } return $this->modules[$moduleName]; } /** * Get the module for an action. * * @param string $action * @return \Codeception\Module|null This method returns null if there is no module for $action */ public function moduleForAction($action) { if (!isset($this->actions[$action])) { return null; } return $this->modules[$this->actions[$action]]; } /** * Get all actions. * * @return array An array with actions as keys and module names as values. */ public function getActions() { return $this->actions; } /** * Get all modules. * * @return array An array with module names as keys and modules as values. */ public function all() { return $this->modules; } /** * Mock a module in this ModuleContainer. * * @param string $moduleName * @param object $mock */ public function mock($moduleName, $mock) { $this->modules[$moduleName] = $mock; } /** * Inject the dependencies of a module. * * @param string $moduleName * @param \Codeception\Lib\Interfaces\DependsOnModule $module * @throws \Codeception\Exception\ModuleException * @throws \Codeception\Exception\ModuleRequireException */ private function injectModuleDependencies($moduleName, DependsOnModule $module) { $this->checkForMissingDependencies($moduleName, $module); if (!method_exists($module, '_inject')) { throw new ModuleException($module, 'Module requires method _inject to be defined to accept dependencies'); } $dependencies = array_map(function ($dependency) { return $this->create($dependency, false); }, $this->getConfiguredDependencies($moduleName)); call_user_func_array([$module, '_inject'], $dependencies); } /** * Check for missing dependencies. * * @param string $moduleName * @param \Codeception\Lib\Interfaces\DependsOnModule $module * @throws \Codeception\Exception\ModuleException * @throws \Codeception\Exception\ModuleRequireException */ private function checkForMissingDependencies($moduleName, DependsOnModule $module) { $dependencies = $this->getModuleDependencies($module); $configuredDependenciesCount = count($this->getConfiguredDependencies($moduleName)); if ($configuredDependenciesCount < count($dependencies)) { $missingDependency = array_keys($dependencies)[$configuredDependenciesCount]; $message = sprintf( "\nThis module depends on %s\n\n\n%s", $missingDependency, $this->getErrorMessageForDependency($module, $missingDependency) ); throw new ModuleRequireException($moduleName, $message); } } /** * Get the dependencies of a module. * * @param \Codeception\Lib\Interfaces\DependsOnModule $module * @return array * @throws \Codeception\Exception\ModuleException */ private function getModuleDependencies(DependsOnModule $module) { $depends = $module->_depends(); if (!$depends) { return []; } if (!is_array($depends)) { $message = sprintf("Method _depends of module '%s' must return an array", get_class($module)); throw new ModuleException($module, $message); } return $depends; } /** * Get the configured dependencies for a module. * * @param string $moduleName * @return array */ private function getConfiguredDependencies($moduleName) { $config = $this->getModuleConfig($moduleName); if (!isset($config['depends'])) { return []; } return is_array($config['depends']) ? $config['depends'] : [$config['depends']]; } /** * Get the error message for a module dependency that is missing. * * @param \Codeception\Module $module * @param string $missingDependency * @return string */ private function getErrorMessageForDependency($module, $missingDependency) { $depends = $module->_depends(); return $depends[$missingDependency]; } /** * Get the configuration for a module. * * A module with name $moduleName can be configured at two paths in a configuration file: * - modules.config.$moduleName * - modules.enabled.$moduleName * * This method checks both locations for configuration. If there is configuration at both locations * this method merges them, where the configuration at modules.enabled.$moduleName takes precedence * over modules.config.$moduleName if the same parameters are configured at both locations. * * @param string $moduleName * @return array */ private function getModuleConfig($moduleName) { $config = isset($this->config['modules']['config'][$moduleName]) ? $this->config['modules']['config'][$moduleName] : []; if (!isset($this->config['modules']['enabled'])) { return $config; } if (!is_array($this->config['modules']['enabled'])) { return $config; } foreach ($this->config['modules']['enabled'] as $enabledModuleConfig) { if (!is_array($enabledModuleConfig)) { continue; } $enabledModuleName = key($enabledModuleConfig); if ($enabledModuleName === $moduleName) { return Configuration::mergeConfigs(reset($enabledModuleConfig), $config); } } return $config; } /** * Check if there are conflicting modules in this ModuleContainer. * * @throws \Codeception\Exception\ModuleConflictException */ public function validateConflicts() { $canConflict = []; foreach ($this->modules as $moduleName => $module) { $parted = $module instanceof PartedModule && $module->_getConfig('part'); if ($this->active[$moduleName] && !$parted) { $canConflict[] = $module; } } foreach ($canConflict as $module) { foreach ($canConflict as $otherModule) { $this->validateConflict($module, $otherModule); } } } /** * Check if the modules passed as arguments to this method conflict with each other. * * @param \Codeception\Module $module * @param \Codeception\Module $otherModule * @throws \Codeception\Exception\ModuleConflictException */ private function validateConflict($module, $otherModule) { if ($module === $otherModule || !$module instanceof ConflictsWithModule) { return; } $conflicts = $this->normalizeConflictSpecification($module->_conflicts()); if ($otherModule instanceof $conflicts) { throw new ModuleConflictException($module, $otherModule); } } /** * Normalize the return value of ConflictsWithModule::_conflicts() to a class name. * This is necessary because it can return a module name instead of the name of a class or interface. * * @param string $conflicts * @return string */ private function normalizeConflictSpecification($conflicts) { if (interface_exists($conflicts) || class_exists($conflicts)) { return $conflicts; } if ($this->hasModule($conflicts)) { return $this->getModule($conflicts); } return $conflicts; } } <?php namespace Codeception\Lib; class Notification { protected static $messages = []; public static function warning($message, $location) { self::$messages[] = 'WARNING: ' . self::formatMessage($message, $location); } public static function deprecate($message, $location = '') { self::$messages[] = 'DEPRECATION: ' . self::formatMessage($message, $location); } private static function formatMessage($message, $location = '') { if ($location) { return "<bold>$message</bold> <info>$location</info>"; } return $message; } public static function all() { $messages = self::$messages; self::$messages = []; return $messages; } } <?php namespace Codeception\Lib; use Codeception\Exception\ConfigurationException; use Symfony\Component\Yaml\Yaml; class ParamsLoader { protected $paramStorage; protected $paramsFile; public function load($paramStorage) { $this->paramsFile = null; $this->paramStorage = $paramStorage; if (is_array($paramStorage)) { return $this->loadArray(); } if ($paramStorage === 'env' || $paramStorage === 'environment') { return $this->loadEnvironmentVars(); } $this->paramsFile = codecept_absolute_path($paramStorage); if (!file_exists($this->paramsFile)) { throw new ConfigurationException("Params file {$this->paramsFile} not found"); } try { if (preg_match('~\.ya?ml$~', $paramStorage)) { return $this->loadYamlFile(); } if (preg_match('~\.ini$~', $paramStorage)) { return $this->loadIniFile(); } if (preg_match('~\.php$~', $paramStorage)) { return $this->loadPhpFile(); } if (preg_match('~(\.env(\.|$))~', $paramStorage)) { return $this->loadDotEnvFile(); } if (preg_match('~\.xml$~', $paramStorage)) { return $this->loadXmlFile(); } } catch (\Exception $e) { throw new ConfigurationException("Failed loading params from $paramStorage\n" . $e->getMessage()); } throw new ConfigurationException("Params can't be loaded from `$paramStorage`."); } public function loadArray() { return $this->paramStorage; } protected function loadIniFile() { return parse_ini_file($this->paramsFile); } protected function loadPhpFile() { return require $this->paramsFile; } protected function loadYamlFile() { $params = Yaml::parse(file_get_contents($this->paramsFile)); if (isset($params['parameters'])) { // Symfony style $params = $params['parameters']; } return $params; } protected function loadXmlFile() { $paramsToArray = function (\SimpleXMLElement $params) use (&$paramsToArray) { $a = []; foreach ($params as $param) { $key = isset($param['key']) ? (string) $param['key'] : $param->getName(); $type = isset($param['type']) ? (string) $param['type'] : 'string'; $value = (string) $param; switch ($type) { case 'bool': case 'boolean': case 'int': case 'integer': case 'float': case 'double': $a[$key] = settype($value, $type); break; case 'constant': $a[$key] = constant($value); break; case 'collection': $a[$key] = $paramsToArray($param); break; default: $a[$key] = (string) $param; } } return $a; }; return $paramsToArray(simplexml_load_file($this->paramsFile)); } protected function loadDotEnvFile() { if (class_exists('Dotenv\Dotenv')) { if (method_exists('Dotenv\Dotenv', 'create')) { //dotenv v3 $dotEnv = \Dotenv\Dotenv::create(codecept_root_dir(), $this->paramStorage); } else { //dotenv v2 $dotEnv = new \Dotenv\Dotenv(codecept_root_dir(), $this->paramStorage); } $dotEnv->load(); return $_SERVER; } elseif (class_exists('Symfony\Component\Dotenv\Dotenv')) { $dotEnv = new \Symfony\Component\Dotenv\Dotenv(); $dotEnv->load(codecept_root_dir($this->paramStorage)); return $_SERVER; } throw new ConfigurationException( "`vlucas/phpdotenv` library is required to parse .env files.\n" . "Please install it via composer: composer require vlucas/phpdotenv" ); } protected function loadEnvironmentVars() { return $_SERVER; } } <?php namespace Codeception\Lib; use Codeception\Configuration; use Codeception\Exception\TestParseException; use Codeception\Scenario; use Codeception\Step; use Codeception\Test\Metadata; class Parser { /** * @var Scenario */ protected $scenario; /** * @var Metadata */ protected $metadata; protected $code; public function __construct(Scenario $scenario, Metadata $metadata) { $this->scenario = $scenario; $this->metadata = $metadata; } public function prepareToRun($code) { $this->parseFeature($code); $this->parseScenarioOptions($code); } public function parseFeature($code) { $matches = []; $code = $this->stripComments($code); $res = preg_match("~\\\$I->wantTo\\(\s*?['\"](.*?)['\"]\s*?\\);~", $code, $matches); if ($res) { $this->scenario->setFeature($matches[1]); return; } $res = preg_match("~\\\$I->wantToTest\\(['\"](.*?)['\"]\\);~", $code, $matches); if ($res) { $this->scenario->setFeature("test " . $matches[1]); return; } } public function parseScenarioOptions($code) { $this->metadata->setParamsFromAnnotations($this->matchComments($code)); } public function parseSteps($code) { // parse per line $friends = []; $lines = explode("\n", $code); $isFriend = false; foreach ($lines as $line) { // friends if (preg_match("~\\\$I->haveFriend\((.*?)\);~", $line, $matches)) { $friends[] = trim($matches[1], '\'"'); } // friend's section start if (preg_match("~\\\$(.*?)->does\(~", $line, $matches)) { $friend = $matches[1]; if (!in_array($friend, $friends)) { continue; } $isFriend = true; $this->addCommentStep("\n----- $friend does -----"); continue; } // actions if (preg_match("~\\\$I->(.*)\((.*?)\);~", $line, $matches)) { $this->addStep($matches); } // friend's section ends if ($isFriend && strpos($line, '}') !== false) { $this->addCommentStep("-------- back to me\n"); $isFriend = false; } } } protected function addStep($matches) { list($m, $action, $params) = $matches; if (in_array($action, ['wantTo', 'wantToTest'])) { return; } $this->scenario->addStep(new Step\Action($action, explode(',', $params))); } protected function addCommentStep($comment) { $this->scenario->addStep(new \Codeception\Step\Comment($comment, [])); } public static function validate($file) { $config = Configuration::config(); if (empty($config['settings']['lint'])) { // lint disabled in config return; } if (!function_exists('exec')) { //exec function is disabled #3324 return; } exec("php -l " . escapeshellarg($file) . " 2>&1", $output, $code); if ($code !== 0) { throw new TestParseException($file, implode("\n", $output)); } } public static function load($file) { if (PHP_MAJOR_VERSION < 7) { self::validate($file); } try { self::includeFile($file); } catch (\ParseError $e) { throw new TestParseException($file, $e->getMessage(), $e->getLine()); } catch (\Exception $e) { // file is valid otherwise } } public static function getClassesFromFile($file) { $sourceCode = file_get_contents($file); $classes = []; $tokens = token_get_all($sourceCode); $tokenCount = count($tokens); $namespace = ''; for ($i = 0; $i < $tokenCount; $i++) { if ($tokens[$i][0] === T_NAMESPACE) { $namespace = ''; for ($j = $i + 1; $j < $tokenCount; $j++) { if ($tokens[$j][0] === T_STRING) { $namespace .= $tokens[$j][1] . '\\'; } else { if ($tokens[$j] === '{' || $tokens[$j] === ';') { break; } } } } if ($tokens[$i][0] === T_CLASS) { if (!isset($tokens[$i - 2])) { $classes[] = $namespace . $tokens[$i + 2][1]; continue; } if ($tokens[$i - 2][0] === T_NEW) { continue; } if ($tokens[$i - 1][0] === T_WHITESPACE and $tokens[$i - 2][0] === T_DOUBLE_COLON) { continue; } if ($tokens[$i - 1][0] === T_DOUBLE_COLON) { continue; } $classes[] = $namespace . $tokens[$i + 2][1]; } } return $classes; } /* * Include in different scope to prevent included file from affecting $file variable */ private static function includeFile($file) { include_once $file; } /** * @param $code * @return mixed */ protected function stripComments($code) { $code = preg_replace('~\/\/.*?$~m', '', $code); // remove inline comments $code = preg_replace('~\/*\*.*?\*\/~ms', '', $code); return $code; // remove block comment } protected function matchComments($code) { $matches = []; $comments = ''; $hasLineComment = preg_match_all('~\/\/(.*?)$~m', $code, $matches); if ($hasLineComment) { foreach ($matches[1] as $line) { $comments .= $line."\n"; } } $hasBlockComment = preg_match('~\/*\*(.*?)\*\/~ms', $code, $matches); if ($hasBlockComment) { $comments .= $matches[1]."\n"; } return $comments; } } <?php namespace Codeception\Lib\Shared; /** * Common functions for Laravel family * * @package Codeception\Lib\Shared */ trait LaravelCommon { /** * Add a binding to the Laravel service container. * (https://laravel.com/docs/master/container) * * ``` php * <?php * $I->haveBinding('My\Interface', 'My\Implementation'); * ?> * ``` * * @param $abstract * @param $concrete */ public function haveBinding($abstract, $concrete) { $this->client->haveBinding($abstract, $concrete); } /** * Add a singleton binding to the Laravel service container. * (https://laravel.com/docs/master/container) * * ``` php * <?php * $I->haveSingleton('My\Interface', 'My\Singleton'); * ?> * ``` * * @param $abstract * @param $concrete */ public function haveSingleton($abstract, $concrete) { $this->client->haveBinding($abstract, $concrete, true); } /** * Add a contextual binding to the Laravel service container. * (https://laravel.com/docs/master/container) * * ``` php * <?php * $I->haveContextualBinding('My\Class', '$variable', 'value'); * * // This is similar to the following in your Laravel application * $app->when('My\Class') * ->needs('$variable') * ->give('value'); * ?> * ``` * * @param $concrete * @param $abstract * @param $implementation */ public function haveContextualBinding($concrete, $abstract, $implementation) { $this->client->haveContextualBinding($concrete, $abstract, $implementation); } /** * Add an instance binding to the Laravel service container. * (https://laravel.com/docs/master/container) * * ``` php * <?php * $I->haveInstance('My\Class', new My\Class()); * ?> * ``` * * @param $abstract * @param $instance */ public function haveInstance($abstract, $instance) { $this->client->haveInstance($abstract, $instance); } /** * Register a handler than can be used to modify the Laravel application object after it is initialized. * The Laravel application object will be passed as an argument to the handler. * * ``` php * <?php * $I->haveApplicationHandler(function($app) { * $app->make('config')->set(['test_value' => '10']); * }); * ?> * ``` * * @param $handler */ public function haveApplicationHandler($handler) { $this->client->haveApplicationHandler($handler); } /** * Clear the registered application handlers. * * ``` php * <?php * $I->clearApplicationHandlers(); * ?> * ``` * */ public function clearApplicationHandlers() { $this->client->clearApplicationHandlers(); } } <?php namespace Codeception\Module; use Codeception\Exception\ModuleException as ModuleException; use Codeception\Lib\Interfaces\RequiresPackage; use Codeception\Module as CodeceptionModule; use Codeception\TestInterface; use Exception; use PhpAmqpLib\Channel\AMQPChannel; use PhpAmqpLib\Connection\AMQPStreamConnection; use PhpAmqpLib\Exception\AMQPProtocolChannelException; use PhpAmqpLib\Message\AMQPMessage; /** * This module interacts with message broker software that implements * the Advanced Message Queuing Protocol (AMQP) standard. For example, RabbitMQ (tested). * * <div class="alert alert-info"> * To use this module with Composer you need <em>"php-amqplib/php-amqplib": "~2.4"</em> package. * </div> * * ## Config * * * host: localhost - host to connect * * username: guest - username to connect * * password: guest - password to connect * * vhost: '/' - vhost to connect * * cleanup: true - defined queues will be purged before running every test. * * queues: [mail, twitter] - queues to cleanup * * single_channel - create and use only one channel during test execution * * ### Example * * modules: * enabled: * - AMQP: * host: 'localhost' * port: '5672' * username: 'guest' * password: 'guest' * vhost: '/' * queues: [queue1, queue2] * single_channel: false * * ## Public Properties * * * connection - AMQPStreamConnection - current connection */ class AMQP extends CodeceptionModule implements RequiresPackage { protected $config = [ 'host' => 'localhost', 'username' => 'guest', 'password' => 'guest', 'port' => '5672', 'vhost' => '/', 'cleanup' => true, 'single_channel' => false, 'queues' => [] ]; /** * @var AMQPStreamConnection */ public $connection; /** * @var int */ protected $channelId; protected $requiredFields = ['host', 'username', 'password', 'vhost']; public function _requires() { return ['PhpAmqpLib\Connection\AMQPStreamConnection' => '"php-amqplib/php-amqplib": "~2.4"']; } public function _initialize() { $host = $this->config['host']; $port = $this->config['port']; $username = $this->config['username']; $password = $this->config['password']; $vhost = $this->config['vhost']; try { $this->connection = new AMQPStreamConnection($host, $port, $username, $password, $vhost); } catch (Exception $e) { throw new ModuleException(__CLASS__, $e->getMessage() . ' while establishing connection to MQ server'); } } public function _before(TestInterface $test) { if ($this->config['cleanup']) { $this->cleanup(); } } /** * Sends message to exchange by sending exchange name, message * and (optionally) a routing key * * ``` php * <?php * $I->pushToExchange('exchange.emails', 'thanks'); * $I->pushToExchange('exchange.emails', new AMQPMessage('Thanks!')); * $I->pushToExchange('exchange.emails', new AMQPMessage('Thanks!'), 'severity'); * ?> * ``` * * @param string $exchange * @param string|\PhpAmqpLib\Message\AMQPMessage $message * @param string $routing_key */ public function pushToExchange($exchange, $message, $routing_key = null) { $message = $message instanceof AMQPMessage ? $message : new AMQPMessage($message); $this->getChannel()->basic_publish($message, $exchange, $routing_key); } /** * Sends message to queue * * ``` php * <?php * $I->pushToQueue('queue.jobs', 'create user'); * $I->pushToQueue('queue.jobs', new AMQPMessage('create')); * ?> * ``` * * @param string $queue * @param string|\PhpAmqpLib\Message\AMQPMessage $message */ public function pushToQueue($queue, $message) { $message = $message instanceof AMQPMessage ? $message : new AMQPMessage($message); $this->getChannel()->queue_declare($queue); $this->getChannel()->basic_publish($message, '', $queue); } /** * Declares an exchange * * This is an alias of method `exchange_declare` of `PhpAmqpLib\Channel\AMQPChannel`. * * ```php * <?php * $I->declareExchange( * 'nameOfMyExchange', // exchange name * 'topic' // exchange type * ) * ``` * * @param string $exchange * @param string $type * @param bool $passive * @param bool $durable * @param bool $auto_delete * @param bool $internal * @param bool $nowait * @param array $arguments * @param int $ticket * @return mixed|null */ public function declareExchange( $exchange, $type, $passive = false, $durable = false, $auto_delete = true, $internal = false, $nowait = false, $arguments = null, $ticket = null ) { return $this->getChannel()->exchange_declare( $exchange, $type, $passive, $durable, $auto_delete, $internal, $nowait, $arguments, $ticket ); } /** * Declares queue, creates if needed * * This is an alias of method `queue_declare` of `PhpAmqpLib\Channel\AMQPChannel`. * * ```php * <?php * $I->declareQueue( * 'nameOfMyQueue', // exchange name * ) * ``` * * @param string $queue * @param bool $passive * @param bool $durable * @param bool $exclusive * @param bool $auto_delete * @param bool $nowait * @param array $arguments * @param int $ticket * @return mixed|null */ public function declareQueue( $queue = '', $passive = false, $durable = false, $exclusive = false, $auto_delete = true, $nowait = false, $arguments = null, $ticket = null ) { return $this->getChannel()->queue_declare( $queue, $passive, $durable, $exclusive, $auto_delete, $nowait, $arguments, $ticket ); } /** * Binds a queue to an exchange * * This is an alias of method `queue_bind` of `PhpAmqpLib\Channel\AMQPChannel`. * * ```php * <?php * $I->bindQueueToExchange( * 'nameOfMyQueueToBind', // name of the queue * 'transactionTracking.transaction', // exchange name to bind to * 'your.routing.key' // Optionally, provide a binding key * ) * ``` * * @param string $queue * @param string $exchange * @param string $routing_key * @param bool $nowait * @param array $arguments * @param int $ticket * @return mixed|null */ public function bindQueueToExchange( $queue, $exchange, $routing_key = '', $nowait = false, $arguments = null, $ticket = null ) { return $this->getChannel()->queue_bind( $queue, $exchange, $routing_key, $nowait, $arguments, $ticket ); } /** * Add a queue to purge list * * @param string $queue */ public function scheduleQueueCleanup($queue) { if (!in_array($queue, $this->config['queues'])) { $this->config['queues'][] = $queue; } } /** * Checks if message containing text received. * * **This method drops message from queue** * **This method will wait for message. If none is sent the script will stuck**. * * ``` php * <?php * $I->pushToQueue('queue.emails', 'Hello, davert'); * $I->seeMessageInQueueContainsText('queue.emails','davert'); * ?> * ``` * * @param string $queue * @param string $text */ public function seeMessageInQueueContainsText($queue, $text) { $msg = $this->getChannel()->basic_get($queue); if (!$msg) { $this->fail("Message was not received"); } if (!$msg instanceof AMQPMessage) { $this->fail("Received message is not format of AMQPMessage"); } $this->debugSection("Message", $msg->body); $this->assertStringContainsString($text, $msg->body); } /** * Count messages in queue. * * @param string $queue * * @return int */ public function _countMessage($queue) { list($queue, $messageCount) = $this->getChannel()->queue_declare($queue, true); return $messageCount; } /** * Checks that queue have expected number of message * * ``` php * <?php * $I->pushToQueue('queue.emails', 'Hello, davert'); * $I->seeNumberOfMessagesInQueue('queue.emails',1); * ?> * ``` * * @param string $queue * @param int $expected */ public function seeNumberOfMessagesInQueue($queue, $expected) { $messageCount = $this->_countMessage($queue); $this->assertEquals($expected, $messageCount); } /** * Checks that queue is empty * * ``` php * <?php * $I->pushToQueue('queue.emails', 'Hello, davert'); * $I->purgeQueue('queue.emails'); * $I->seeQueueIsEmpty('queue.emails'); * ?> * ``` * * @param string $queue * @param int $expected */ public function seeQueueIsEmpty($queue) { $messageCount = $this->_countMessage($queue); $this->assertEquals(0, $messageCount); } /** * Checks if queue is not empty. * * ``` php * <?php * $I->pushToQueue('queue.emails', 'Hello, davert'); * $I->dontSeeQueueIsEmpty('queue.emails'); * ?> * ``` * * @param string $queue */ public function dontSeeQueueIsEmpty($queue) { $messageCount = $this->_countMessage($queue); $this->assertNotEquals(0, $messageCount); } /** * Takes last message from queue. * * ``` php * <?php * $message = $I->grabMessageFromQueue('queue.emails'); * ?> * ``` * * @param string $queue * @return \PhpAmqpLib\Message\AMQPMessage */ public function grabMessageFromQueue($queue) { $message = $this->getChannel()->basic_get($queue); return $message; } /** * Purge a specific queue defined in config. * * ``` php * <?php * $I->purgeQueue('queue.emails'); * ?> * ``` * * @param string $queueName */ public function purgeQueue($queueName = '') { if (! in_array($queueName, $this->config['queues'])) { throw new ModuleException(__CLASS__, "'$queueName' doesn't exist in queues config list"); } $this->getChannel()->queue_purge($queueName, true); } /** * Purge all queues defined in config. * * ``` php * <?php * $I->purgeAllQueues(); * ?> * ``` */ public function purgeAllQueues() { $this->cleanup(); } /** * @return \PhpAmqpLib\Channel\AMQPChannel */ protected function getChannel() { if ($this->config['single_channel'] && $this->channelId === null) { $this->channelId = $this->connection->get_free_channel_id(); } return $this->connection->channel($this->channelId); } protected function cleanup() { if (!isset($this->config['queues'])) { throw new ModuleException(__CLASS__, "please set queues for cleanup"); } if (!$this->connection) { return; } foreach ($this->config['queues'] as $queue) { try { $this->getChannel()->queue_purge($queue); } catch (AMQPProtocolChannelException $e) { // ignore if exchange/queue doesn't exist and rethrow exception if it's something else if ($e->getCode() !== 404) { throw $e; } } } } } <?php namespace Codeception\Module; use Codeception\Module; use Codeception\TestInterface; use Codeception\Exception\ModuleException; /** * This module interacts with the [Alternative PHP Cache (APC)](http://php.net/manual/en/intro.apcu.php) * using either _APCu_ or _APC_ extension. * * Performs a cleanup by flushing all values after each test run. * * ## Status * * * Maintainer: **Serghei Iakovlev** * * Stability: **stable** * * Contact: serghei@phalconphp.com * * ### Example (`unit.suite.yml`) * * ```yaml * modules: * - Apc * ``` * * Be sure you don't use the production server to connect. * */ class Apc extends Module { /** * Code to run before each test. * * @param TestInterface $test * @throws ModuleException */ public function _before(TestInterface $test) { if (!extension_loaded('apc') && !extension_loaded('apcu')) { throw new ModuleException( __CLASS__, 'The APC(u) extension not loaded.' ); } if (!ini_get('apc.enabled') || (PHP_SAPI === 'cli' && !ini_get('apc.enable_cli'))) { throw new ModuleException( __CLASS__, 'The "apc.enable_cli" parameter must be set to "On".' ); } } /** * Code to run after each test. * * @param TestInterface $test */ public function _after(TestInterface $test) { $this->clear(); } /** * Grabs value from APC(u) by key. * * Example: * * ``` php * <?php * $users_count = $I->grabValueFromApc('users_count'); * ?> * ``` * * @param string|string[] $key * @return mixed */ public function grabValueFromApc($key) { $value = $this->fetch($key); $this->debugSection('Value', $value); return $value; } /** * Checks item in APC(u) exists and the same as expected. * * Examples: * * ``` php * <?php * // With only one argument, only checks the key exists * $I->seeInApc('users_count'); * * // Checks a 'users_count' exists and has the value 200 * $I->seeInApc('users_count', 200); * ?> * ``` * * @param string|string[] $key * @param mixed $value */ public function seeInApc($key, $value = null) { if (null === $value) { $this->assertTrue($this->exists($key), "Cannot find key '$key' in APC(u)."); return; } $actual = $this->grabValueFromApc($key); $this->assertEquals($value, $actual, "Cannot find key '$key' in APC(u) with the provided value."); } /** * Checks item in APC(u) doesn't exist or is the same as expected. * * Examples: * * ``` php * <?php * // With only one argument, only checks the key does not exist * $I->dontSeeInApc('users_count'); * * // Checks a 'users_count' exists does not exist or its value is not the one provided * $I->dontSeeInApc('users_count', 200); * ?> * ``` * * @param string|string[] $key * @param mixed $value */ public function dontSeeInApc($key, $value = null) { if (null === $value) { $this->assertFalse($this->exists($key), "The key '$key' exists in APC(u)."); return; } $actual = $this->grabValueFromApc($key); if (false !== $actual) { $this->assertEquals($value, $actual, "The key '$key' exists in APC(u) with the provided value."); } } /** * Stores an item `$value` with `$key` on the APC(u). * * Examples: * * ```php * <?php * // Array * $I->haveInApc('users', ['name' => 'miles', 'email' => 'miles@davis.com']); * * // Object * $I->haveInApc('user', UserRepository::findFirst()); * * // Key as array of 'key => value' * $entries = []; * $entries['key1'] = 'value1'; * $entries['key2'] = 'value2'; * $entries['key3'] = ['value3a','value3b']; * $entries['key4'] = 4; * $I->haveInApc($entries, null); * ?> * ``` * * @param string|array $key * @param mixed $value * @param int $expiration * @return mixed */ public function haveInApc($key, $value, $expiration = null) { $this->store($key, $value, $expiration); return $key; } /** * Clears the APC(u) cache */ public function flushApc() { // Returns TRUE always $this->clear(); } /** * Clears the APC(u) cache. * * @return bool */ protected function clear() { if (function_exists('apcu_clear_cache')) { return apcu_clear_cache(); } return apc_clear_cache('user'); } /** * Checks if entry exists * * @param string|string[] $key * * @return bool|\string[] */ protected function exists($key) { if (function_exists('apcu_exists')) { return apcu_exists($key); } return apc_exists($key); } /** * Fetch a stored variable from the cache * * @param string|string[] $key * * @return mixed */ protected function fetch($key) { $success = false; if (function_exists('apcu_fetch')) { $data = apcu_fetch($key, $success); } else { $data = apc_fetch($key, $success); } $this->debugSection('Fetching a stored variable', $success ? 'OK' : 'FAILED'); return $data; } /** * Cache a variable in the data store. * * @param string|array $key * @param mixed $var * @param int $ttl * * @return array|bool */ protected function store($key, $var, $ttl = 0) { if (function_exists('apcu_store')) { return apcu_store($key, $var, $ttl); } return apc_store($key, $var, $ttl); } } <?php namespace Codeception\Module; use Codeception\Module as CodeceptionModule; use Codeception\Util\Shared\Asserts as SharedAsserts; /** * Special module for using asserts in your tests. */ class Asserts extends CodeceptionModule { use SharedAsserts { assertEquals as public; assertNotEquals as public; assertSame as public; assertNotSame as public; assertGreaterThan as public; assertGreaterThanOrEqual as public; assertLessThan as public; assertLessThanOrEqual as public; assertContains as public; assertNotContains as public; assertRegExp as public; assertNotRegExp as public; assertEmpty as public; assertNotEmpty as public; assertNull as public; assertNotNull as public; assertTrue as public; assertFalse as public; assertFileExists as public; assertFileNotExists as public; assertGreaterOrEquals as public; assertLessOrEquals as public; assertIsEmpty as public; assertArrayHasKey as public; assertArrayNotHasKey as public; assertInstanceOf as public; assertNotInstanceOf as public; assertInternalType as public; assertCount as public; assertStringStartsWith as public; assertStringStartsNotWith as public; assertNotTrue as public; assertNotFalse as public; assertStringContainsString as public; assertStringContainsStringIgnoringCase as public; assertStringNotContainsString as public; assertStringNotContainsStringIgnoringCase as public; assertIsArray as public; assertIsBool as public; assertIsFloat as public; assertIsInt as public; assertIsNumeric as public; assertIsObject as public; assertIsResource as public; assertIsString as public; assertIsScalar as public; assertIsCallable as public; assertIsNotArray as public; assertIsNotBool as public; assertIsNotFloat as public; assertIsNotInt as public; assertIsNotNumeric as public; assertIsNotObject as public; assertIsNotResource as public; assertIsNotString as public; assertIsNotScalar as public; assertIsNotCallable as public; fail as public; } /** * Handles and checks exception called inside callback function. * Either exception class name or exception instance should be provided. * * ```php * <?php * $I->expectException(MyException::class, function() { * $this->doSomethingBad(); * }); * * $I->expectException(new MyException(), function() { * $this->doSomethingBad(); * }); * ``` * If you want to check message or exception code, you can pass them with exception instance: * ```php * <?php * // will check that exception MyException is thrown with "Don't do bad things" message * $I->expectException(new MyException("Don't do bad things"), function() { * $this->doSomethingBad(); * }); * ``` * * @param $exception string or \Exception * @param $callback */ public function expectException($exception, $callback) { $this->expectThrowable($exception, $callback); } /** * Handles and checks throwables (Exceptions/Errors) called inside the callback function. * Either throwable class name or throwable instance should be provided. * * ```php * <?php * $I->expectThrowable(MyThrowable::class, function() { * $this->doSomethingBad(); * }); * * $I->expectThrowable(new MyException(), function() { * $this->doSomethingBad(); * }); * ``` * If you want to check message or throwable code, you can pass them with throwable instance: * ```php * <?php * // will check that throwable MyError is thrown with "Don't do bad things" message * $I->expectThrowable(new MyError("Don't do bad things"), function() { * $this->doSomethingBad(); * }); * ``` * * @param $throwable string or \Throwable * @param $callback */ public function expectThrowable($throwable, $callback) { if (is_object($throwable)) { /** @var $throwable \Throwable */ $class = get_class($throwable); $msg = $throwable->getMessage(); $code = $throwable->getCode(); } else { $class= $throwable; $msg = null; $code = null; } try { $callback(); } catch (\Exception $t) { $this->checkThrowable($t, $class, $msg, $code); return; } catch (\Throwable $t) { $this->checkThrowable($t, $class, $msg, $code); return; } $this->fail("Expected throwable of class '$class' to be thrown, but nothing was caught"); } /** * Check if the given throwable matches the expected data, * fail (throws an exception) if it does not. * * @param \Throwable $throwable * @param string $expectedClass * @param string $expectedMsg * @param int $expectedCode */ protected function checkThrowable($throwable, $expectedClass, $expectedMsg, $expectedCode) { if (!($throwable instanceof $expectedClass)) { $this->fail(sprintf( "Exception of class '$expectedClass' expected to be thrown, but class '%s' was caught", get_class($throwable) )); } if (null !== $expectedMsg && $throwable->getMessage() !== $expectedMsg) { $this->fail(sprintf( "Exception of class '$expectedClass' expected to have message '$expectedMsg', but actual message was '%s'", $throwable->getMessage() )); } if (null !== $expectedCode && $throwable->getCode() !== $expectedCode) { $this->fail(sprintf( "Exception of class '$expectedClass' expected to have code '$expectedCode', but actual code was '%s'", $throwable->getCode() )); } $this->assertTrue(true); // increment assertion counter } } <?php namespace Codeception\Module; use Codeception\Module as CodeceptionModule; use Codeception\TestInterface; /** * Wrapper for basic shell commands and shell output * * ## Responsibility * * Maintainer: **davert** * * Status: **stable** * * Contact: codecept@davert.mail.ua * * *Please review the code of non-stable modules and provide patches if you have issues.* */ class Cli extends CodeceptionModule { public $output = ''; public $result = null; public function _before(TestInterface $test) { $this->output = ''; } /** * Executes a shell command. * Fails If exit code is > 0. You can disable this by setting second parameter to false * * ```php * <?php * $I->runShellCommand('phpunit'); * * // do not fail test when command fails * $I->runShellCommand('phpunit', false); * ``` * * @param $command * @param bool $failNonZero */ public function runShellCommand($command, $failNonZero = true) { $data = []; exec("$command", $data, $resultCode); $this->result = $resultCode; $this->output = implode("\n", $data); if ($this->output === null) { \PHPUnit\Framework\Assert::fail("$command can't be executed"); } if ($resultCode !== 0 && $failNonZero) { \PHPUnit\Framework\Assert::fail("Result code was $resultCode.\n\n" . $this->output); } $this->debug(preg_replace('~s/\e\[\d+(?>(;\d+)*)m//g~', '', $this->output)); } /** * Checks that output from last executed command contains text * * @param $text */ public function seeInShellOutput($text) { \Codeception\PHPUnit\TestCase::assertStringContainsString($text, $this->output); } /** * Checks that output from latest command doesn't contain text * * @param $text * */ public function dontSeeInShellOutput($text) { $this->debug($this->output); \PHPUnit\Framework\Assert::assertNotContains($text, $this->output); } /** * @param $regex */ public function seeShellOutputMatches($regex) { \PHPUnit\Framework\Assert::assertRegExp($regex, $this->output); } /** * Checks result code * * ```php * <?php * $I->seeResultCodeIs(0); * ``` * * @param $code */ public function seeResultCodeIs($code) { $this->assertEquals($this->result, $code, "result code is $code"); } /** * Checks result code * * ```php * <?php * $I->seeResultCodeIsNot(0); * ``` * * @param $code */ public function seeResultCodeIsNot($code) { $this->assertNotEquals($this->result, $code, "result code is $code"); } } <?php namespace Codeception\Module; use Codeception\Lib\Interfaces\DataMapper; use Codeception\Lib\Interfaces\DependsOnModule; use Codeception\Lib\Interfaces\ORM; use Codeception\Exception\ModuleException; use Codeception\Lib\Interfaces\RequiresPackage; use Codeception\TestInterface; use League\FactoryMuffin\FactoryMuffin; use League\FactoryMuffin\Stores\RepositoryStore; /** * DataFactory allows you to easily generate and create test data using [**FactoryMuffin**](https://github.com/thephpleague/factory-muffin). * DataFactory uses an ORM of your application to define, save and cleanup data. Thus, should be used with ORM or Framework modules. * * This module requires packages installed: * * ```json * { * "league/factory-muffin": "^3.0", * } * ``` * * Generation rules can be defined in a factories file. You will need to create `factories.php` (it is recommended to store it in `_support` dir) * Follow [FactoryMuffin documentation](https://github.com/thephpleague/factory-muffin) to set valid rules. * Random data provided by [Faker](https://github.com/fzaninotto/Faker) library. * * ```php * <?php * use League\FactoryMuffin\Faker\Facade as Faker; * * $fm->define(User::class)->setDefinitions([ * 'name' => Faker::name(), * * // generate email * 'email' => Faker::email(), * 'body' => Faker::text(), * * // generate a profile and return its Id * 'profile_id' => 'factory|Profile' * ]); * ``` * * Configure this module to load factory definitions from a directory. * You should also specify a module with an ORM as a dependency. * * ```yaml * modules: * enabled: * - Yii2: * configFile: path/to/config.php * - DataFactory: * factories: tests/_support/factories * depends: Yii2 * ``` * * (you can also use Laravel5 and Phalcon). * * In this example factories are loaded from `tests/_support/factories` directory. Please note that this directory is relative from the codeception.yml file (so for Yii2 it would be codeception/_support/factories). * You should create this directory manually and create PHP files in it with factories definitions following [official documentation](https://github.com/thephpleague/factory-muffin#usage). * * In cases you want to use data from database inside your factory definitions you can define them in Helper. * For instance, if you use Doctrine, this allows you to access `EntityManager` inside a definition. * * To proceed you should create Factories helper via `generate:helper` command and enable it: * * ``` * modules: * enabled: * - DataFactory: * depends: Doctrine2 * - \Helper\Factories * * ``` * * In this case you can define factories from a Helper class with `_define` method. * * ```php * <?php * public function _beforeSuite() * { * $factory = $this->getModule('DataFactory'); * // let us get EntityManager from Doctrine * $em = $this->getModule('Doctrine2')->_getEntityManager(); * * $factory->_define(User::class, [ * * // generate random user name * // use League\FactoryMuffin\Faker\Facade as Faker; * 'name' => Faker::name(), * * // get real company from database * 'company' => $em->getRepository(Company::class)->find(), * * // let's generate a profile for each created user * // receive an entity and set it via `setProfile` method * // UserProfile factory should be defined as well * 'profile' => 'entity|'.UserProfile::class * ]); * } * ``` * * Factory Definitions are described in official [Factory Muffin Documentation](https://github.com/thephpleague/factory-muffin) * * ### Related Models Generators * * If your module relies on other model you can generate them both. * To create a related module you can use either `factory` or `entity` prefix, depending on ORM you use. * * In case your ORM expects an Id of a related record (Eloquent) to be set use `factory` prefix: * * ```php * 'user_id' => 'factory|User' * ``` * * In case your ORM expects a related record itself (Doctrine) then you should use `entity` prefix: * * ```php * 'user' => 'entity|User' * ``` */ class DataFactory extends \Codeception\Module implements DependsOnModule, RequiresPackage { protected $dependencyMessage = <<<EOF ORM module (like Doctrine2) or Framework module with ActiveRecord support is required: -- modules: enabled: - DataFactory: depends: Doctrine2 -- EOF; /** * ORM module on which we we depend on. * * @var ORM */ public $ormModule; /** * @var FactoryMuffin */ public $factoryMuffin; protected $config = ['factories' => null]; public function _requires() { return [ 'League\FactoryMuffin\FactoryMuffin' => '"league/factory-muffin": "^3.0"', ]; } public function _beforeSuite($settings = []) { $store = $this->getStore(); $this->factoryMuffin = new FactoryMuffin($store); if ($this->config['factories']) { foreach ((array) $this->config['factories'] as $factoryPath) { $realpath = realpath(codecept_root_dir().$factoryPath); if ($realpath === false) { throw new ModuleException($this, 'The path to one of your factories is not correct. Please specify the directory relative to the codeception.yml file (ie. _support/factories).'); } $this->factoryMuffin->loadFactories($realpath); } } } /** * @return StoreInterface|null */ protected function getStore() { return $this->ormModule instanceof DataMapper ? new RepositoryStore($this->ormModule->_getEntityManager()) // for Doctrine : null; } public function _inject(ORM $orm) { $this->ormModule = $orm; } public function _after(TestInterface $test) { $skipCleanup = array_key_exists('cleanup', $this->config) && $this->config['cleanup'] === false; if ($skipCleanup || $this->ormModule->_getConfig('cleanup')) { return; } $this->factoryMuffin->deleteSaved(); } public function _depends() { return ['Codeception\Lib\Interfaces\ORM' => $this->dependencyMessage]; } /** * @throws ModuleException */ public function onReconfigure($settings = []) { $skipCleanup = array_key_exists('cleanup', $this->config) && $this->config['cleanup'] === false; if (!$skipCleanup && !$this->ormModule->_getConfig('cleanup')) { $this->factoryMuffin->deleteSaved(); } $this->_beforeSuite($settings); } /** * Creates a model definition. This can be used from a helper:. * * ```php * $this->getModule('{{MODULE_NAME}}')->_define('User', [ * 'name' => $faker->name, * 'email' => $faker->email * ]); * * ``` * * @param string $model * @param array $fields * * @return \League\FactoryMuffin\Definition * * @throws \League\FactoryMuffin\Exceptions\DefinitionAlreadyDefinedException */ public function _define($model, $fields) { return $this->factoryMuffin->define($model)->setDefinitions($fields); } /** * Generates and saves a record,. * * ```php * $I->have('User'); // creates user * $I->have('User', ['is_active' => true]); // creates active user * ``` * * Returns an instance of created user. * * @param string $name * @param array $extraAttrs * * @return object */ public function have($name, array $extraAttrs = []) { return $this->factoryMuffin->create($name, $extraAttrs); } /** * Generates a record instance. * * This does not save it in the database. Use `have` for that. * * ```php * $user = $I->make('User'); // return User instance * $activeUser = $I->make('User', ['is_active' => true]); // return active user instance * ``` * * Returns an instance of created user without creating a record in database. * * @param string $name * @param array $extraAttrs * * @return object */ public function make($name, array $extraAttrs = []) { return $this->factoryMuffin->instance($name, $extraAttrs); } /** * Generates and saves a record multiple times. * * ```php * $I->haveMultiple('User', 10); // create 10 users * $I->haveMultiple('User', 10, ['is_active' => true]); // create 10 active users * ``` * * @param string $name * @param int $times * @param array $extraAttrs * * @return \object[] */ public function haveMultiple($name, $times, array $extraAttrs = []) { return $this->factoryMuffin->seed($times, $name, $extraAttrs); } } <?php namespace Codeception\Module; use Codeception\Module as CodeceptionModule; use Codeception\Configuration; use Codeception\Exception\ModuleException; use Codeception\Exception\ModuleConfigException; use Codeception\Lib\Interfaces\Db as DbInterface; use Codeception\Lib\Driver\Db as Driver; use Codeception\Lib\DbPopulator; use Codeception\TestInterface; use Codeception\Lib\Notification; use Codeception\Util\ActionSequence; /** * Access a database. * * The most important function of this module is to clean a database before each test. * This module also provides actions to perform checks in a database, e.g. [seeInDatabase()](http://codeception.com/docs/modules/Db#seeInDatabase) * * In order to have your database populated with data you need a raw SQL dump. * Simply put the dump in the `tests/_data` directory (by default) and specify the path in the config. * The next time after the database is cleared, all your data will be restored from the dump. * Don't forget to include `CREATE TABLE` statements in the dump. * * Supported and tested databases are: * * * MySQL * * SQLite (i.e. just one file) * * PostgreSQL * * Also available: * * * MS SQL * * Oracle * * Connection is done by database Drivers, which are stored in the `Codeception\Lib\Driver` namespace. * [Check out the drivers](https://github.com/Codeception/Codeception/tree/2.4/src/Codeception/Lib/Driver) * if you run into problems loading dumps and cleaning databases. * * ## Config * * * dsn *required* - PDO DSN * * user *required* - username to access database * * password *required* - password * * dump - path to database dump * * populate: false - whether the the dump should be loaded before the test suite is started * * cleanup: false - whether the dump should be reloaded before each test * * reconnect: false - whether the module should reconnect to the database before each test * * waitlock: 0 - wait lock (in seconds) that the database session should use for DDL statements * * ssl_key - path to the SSL key (MySQL specific, @see http://php.net/manual/de/ref.pdo-mysql.php#pdo.constants.mysql-attr-key) * * ssl_cert - path to the SSL certificate (MySQL specific, @see http://php.net/manual/de/ref.pdo-mysql.php#pdo.constants.mysql-attr-ssl-cert) * * ssl_ca - path to the SSL certificate authority (MySQL specific, @see http://php.net/manual/de/ref.pdo-mysql.php#pdo.constants.mysql-attr-ssl-ca) * * ssl_verify_server_cert - disables certificate CN verification (MySQL specific, @see http://php.net/manual/de/ref.pdo-mysql.php) * * ssl_cipher - list of one or more permissible ciphers to use for SSL encryption (MySQL specific, @see http://php.net/manual/de/ref.pdo-mysql.php#pdo.constants.mysql-attr-cipher) * * databases - include more database configs and switch between them in tests. * * ## Example * * modules: * enabled: * - Db: * dsn: 'mysql:host=localhost;dbname=testdb' * user: 'root' * password: '' * dump: 'tests/_data/dump.sql' * populate: true * cleanup: true * reconnect: true * waitlock: 10 * ssl_key: '/path/to/client-key.pem' * ssl_cert: '/path/to/client-cert.pem' * ssl_ca: '/path/to/ca-cert.pem' * ssl_verify_server_cert: false * ssl_cipher: 'AES256-SHA' * * ## Example with multi-dumps * modules: * enabled: * - Db: * dsn: 'mysql:host=localhost;dbname=testdb' * user: 'root' * password: '' * dump: * - 'tests/_data/dump.sql' * - 'tests/_data/dump-2.sql' * * ## Example with multi-databases * * modules: * enabled: * - Db: * dsn: 'mysql:host=localhost;dbname=testdb' * user: 'root' * password: '' * databases: * db2: * dsn: 'mysql:host=localhost;dbname=testdb2' * user: 'userdb2' * password: '' * * ## SQL data dump * * There are two ways of loading the dump into your database: * * ### Populator * * The recommended approach is to configure a `populator`, an external command to load a dump. Command parameters like host, username, password, database * can be obtained from the config and inserted into placeholders: * * For MySQL: * * ```yaml * modules: * enabled: * - Db: * dsn: 'mysql:host=localhost;dbname=testdb' * user: 'root' * password: '' * dump: 'tests/_data/dump.sql' * populate: true # run populator before all tests * cleanup: true # run populator before each test * populator: 'mysql -u $user -h $host $dbname < $dump' * ``` * * For PostgreSQL (using pg_restore) * * ``` * modules: * enabled: * - Db: * dsn: 'pgsql:host=localhost;dbname=testdb' * user: 'root' * password: '' * dump: 'tests/_data/db_backup.dump' * populate: true # run populator before all tests * cleanup: true # run populator before each test * populator: 'pg_restore -u $user -h $host -D $dbname < $dump' * ``` * * Variable names are being taken from config and DSN which has a `keyword=value` format, so you should expect to have a variable named as the * keyword with the full value inside it. * * PDO dsn elements for the supported drivers: * * MySQL: [PDO_MYSQL DSN](https://secure.php.net/manual/en/ref.pdo-mysql.connection.php) * * SQLite: [PDO_SQLITE DSN](https://secure.php.net/manual/en/ref.pdo-sqlite.connection.php) * * PostgreSQL: [PDO_PGSQL DSN](https://secure.php.net/manual/en/ref.pdo-pgsql.connection.php) * * MSSQL: [PDO_SQLSRV DSN](https://secure.php.net/manual/en/ref.pdo-sqlsrv.connection.php) * * Oracle: [PDO_OCI DSN](https://secure.php.net/manual/en/ref.pdo-oci.connection.php) * * ### Dump * * Db module by itself can load SQL dump without external tools by using current database connection. * This approach is system-independent, however, it is slower than using a populator and may have parsing issues (see below). * * Provide a path to SQL file in `dump` config option: * * ```yaml * modules: * enabled: * - Db: * dsn: 'mysql:host=localhost;dbname=testdb' * user: 'root' * password: '' * populate: true # load dump before all tests * cleanup: true # load dump for each test * dump: 'tests/_data/dump.sql' * ``` * * To parse SQL Db file, it should follow this specification: * * Comments are permitted. * * The `dump.sql` may contain multiline statements. * * The delimiter, a semi-colon in this case, must be on the same line as the last statement: * * ```sql * -- Add a few contacts to the table. * REPLACE INTO `Contacts` (`created`, `modified`, `status`, `contact`, `first`, `last`) VALUES * (NOW(), NOW(), 1, 'Bob Ross', 'Bob', 'Ross'), * (NOW(), NOW(), 1, 'Fred Flintstone', 'Fred', 'Flintstone'); * * -- Remove existing orders for testing. * DELETE FROM `Order`; * ``` * ## Query generation * * `seeInDatabase`, `dontSeeInDatabase`, `seeNumRecords`, `grabFromDatabase` and `grabNumRecords` methods * accept arrays as criteria. WHERE condition is generated using item key as a field name and * item value as a field value. * * Example: * ```php * <?php * $I->seeInDatabase('users', ['name' => 'Davert', 'email' => 'davert@mail.com']); * * ``` * Will generate: * * ```sql * SELECT COUNT(*) FROM `users` WHERE `name` = 'Davert' AND `email` = 'davert@mail.com' * ``` * Since version 2.1.9 it's possible to use LIKE in a condition, as shown here: * * ```php * <?php * $I->seeInDatabase('users', ['name' => 'Davert', 'email like' => 'davert%']); * * ``` * Will generate: * * ```sql * SELECT COUNT(*) FROM `users` WHERE `name` = 'Davert' AND `email` LIKE 'davert%' * ``` * ## Public Properties * * dbh - contains the PDO connection * * driver - contains the Connection Driver * */ class Db extends CodeceptionModule implements DbInterface { /** * @var array */ protected $config = [ 'populate' => false, 'cleanup' => false, 'reconnect' => false, 'waitlock' => 0, 'dump' => null, 'populator' => null, ]; /** * @var array */ protected $requiredFields = ['dsn', 'user', 'password']; const DEFAULT_DATABASE = 'default'; /** * @var Driver[] */ public $drivers = []; /** * @var \PDO[] */ public $dbhs = []; public $databasesPopulated = []; public $databasesSql = []; protected $insertedRows = []; public $currentDatabase = self::DEFAULT_DATABASE; protected function getDatabases() { $databases = [$this->currentDatabase => $this->config]; if (!empty($this->config['databases'])) { foreach ($this->config['databases'] as $databaseKey => $databaseConfig) { $databases[$databaseKey] = array_merge([ 'populate' => false, 'cleanup' => false, 'reconnect' => false, 'waitlock' => 0, 'dump' => null, 'populator' => null, ], $databaseConfig); } } return $databases; } protected function connectToDatabases() { foreach ($this->getDatabases() as $databaseKey => $databaseConfig) { $this->connect($databaseKey, $databaseConfig); } } protected function cleanUpDatabases() { foreach ($this->getDatabases() as $databaseKey => $databaseConfig) { $this->_cleanup($databaseKey, $databaseConfig); } } protected function populateDatabases($configKey) { foreach ($this->getDatabases() as $databaseKey => $databaseConfig) { if ($databaseConfig[$configKey]) { if (!$databaseConfig['populate']) { return; } if (isset($this->databasesPopulated[$databaseKey]) && $this->databasesPopulated[$databaseKey]) { return; } $this->_loadDump($databaseKey, $databaseConfig); } } } protected function readSqlForDatabases() { foreach ($this->getDatabases() as $databaseKey => $databaseConfig) { $this->readSql($databaseKey, $databaseConfig); } } protected function removeInsertedForDatabases() { foreach ($this->getDatabases() as $databaseKey => $databaseConfig) { $this->amConnectedToDatabase($databaseKey); $this->removeInserted($databaseKey); } } protected function disconnectDatabases() { foreach ($this->getDatabases() as $databaseKey => $databaseConfig) { $this->disconnect($databaseKey); } } protected function reconnectDatabases() { foreach ($this->getDatabases() as $databaseKey => $databaseConfig) { if ($databaseConfig['reconnect']) { $this->disconnect($databaseKey); $this->connect($databaseKey, $databaseConfig); } } } public function __get($name) { Notification::deprecate("Properties dbh and driver are deprecated in favor of Db::_getDbh and Db::_getDriver", "Db module"); if ($name == 'driver') { return $this->_getDriver(); } if ($name == 'dbh') { return $this->_getDbh(); } } /** * @return Driver */ public function _getDriver() { return $this->drivers[$this->currentDatabase]; } public function _getDbh() { return $this->dbhs[$this->currentDatabase]; } /** * Make sure you are connected to the right database. * * ```php * <?php * $I->seeNumRecords(2, 'users'); //executed on default database * $I->amConnectedToDatabase('db_books'); * $I->seeNumRecords(30, 'books'); //executed on db_books database * //All the next queries will be on db_books * ``` * @param $databaseKey * @throws ModuleConfigException */ public function amConnectedToDatabase($databaseKey) { if (empty($this->getDatabases()[$databaseKey]) && $databaseKey != self::DEFAULT_DATABASE) { throw new ModuleConfigException( __CLASS__, "\nNo database $databaseKey in the key databases.\n" ); } $this->currentDatabase = $databaseKey; } /** * Can be used with a callback if you don't want to change the current database in your test. * * ```php * <?php * $I->seeNumRecords(2, 'users'); //executed on default database * $I->performInDatabase('db_books', function($I) { * $I->seeNumRecords(30, 'books'); //executed on db_books database * }); * $I->seeNumRecords(2, 'users'); //executed on default database * ``` * List of actions can be pragmatically built using `Codeception\Util\ActionSequence`: * * ```php * <?php * $I->performInDatabase('db_books', ActionSequence::build() * ->seeNumRecords(30, 'books') * ); * ``` * Alternatively an array can be used: * * ```php * $I->performInDatabase('db_books', ['seeNumRecords' => [30, 'books']]); * ``` * * Choose the syntax you like the most and use it, * * Actions executed from array or ActionSequence will print debug output for actions, and adds an action name to * exception on failure. * * @param $databaseKey * @param \Codeception\Util\ActionSequence|array|callable $actions * @throws ModuleConfigException */ public function performInDatabase($databaseKey, $actions) { $backupDatabase = $this->currentDatabase; $this->amConnectedToDatabase($databaseKey); if (is_callable($actions)) { $actions($this); $this->amConnectedToDatabase($backupDatabase); return; } if (is_array($actions)) { $actions = ActionSequence::build()->fromArray($actions); } if (!$actions instanceof ActionSequence) { throw new \InvalidArgumentException("2nd parameter, actions should be callback, ActionSequence or array"); } $actions->run($this); $this->amConnectedToDatabase($backupDatabase); } public function _initialize() { $this->connectToDatabases(); } public function __destruct() { $this->disconnectDatabases(); } public function _beforeSuite($settings = []) { $this->readSqlForDatabases(); $this->connectToDatabases(); $this->cleanUpDatabases(); $this->populateDatabases('populate'); } private function readSql($databaseKey = null, $databaseConfig = null) { if ($databaseConfig['populator']) { return; } if (!$databaseConfig['cleanup'] && !$databaseConfig['populate']) { return; } if (empty($databaseConfig['dump'])) { return; } if (!is_array($databaseConfig['dump'])) { $databaseConfig['dump'] = [$databaseConfig['dump']]; } $sql = ''; foreach ($databaseConfig['dump'] as $filePath) { $sql .= $this->readSqlFile($filePath); } if (!empty($sql)) { // split SQL dump into lines $this->databasesSql[$databaseKey] = preg_split('/\r\n|\n|\r/', $sql, -1, PREG_SPLIT_NO_EMPTY); } } /** * @param $filePath * * @return bool|null|string|string[] * @throws \Codeception\Exception\ModuleConfigException */ private function readSqlFile($filePath) { if (!file_exists(Configuration::projectDir() . $filePath)) { throw new ModuleConfigException( __CLASS__, "\nFile with dump doesn't exist.\n" . "Please, check path for sql file: " . $filePath ); } $sql = file_get_contents(Configuration::projectDir() . $filePath); // remove C-style comments (except MySQL directives) $sql = preg_replace('%/\*(?!!\d+).*?\*/%s', '', $sql); return $sql; } private function connect($databaseKey, $databaseConfig) { if (!empty($this->drivers[$databaseKey]) && !empty($this->dbhs[$databaseKey])) { return; } $options = []; /** * @see http://php.net/manual/en/pdo.construct.php * @see http://php.net/manual/de/ref.pdo-mysql.php#pdo-mysql.constants */ if (array_key_exists('ssl_key', $databaseConfig) && !empty($databaseConfig['ssl_key']) && defined('\PDO::MYSQL_ATTR_SSL_KEY') ) { $options[\PDO::MYSQL_ATTR_SSL_KEY] = (string) $databaseConfig['ssl_key']; } if (array_key_exists('ssl_cert', $databaseConfig) && !empty($databaseConfig['ssl_cert']) && defined('\PDO::MYSQL_ATTR_SSL_CERT') ) { $options[\PDO::MYSQL_ATTR_SSL_CERT] = (string) $databaseConfig['ssl_cert']; } if (array_key_exists('ssl_ca', $databaseConfig) && !empty($databaseConfig['ssl_ca']) && defined('\PDO::MYSQL_ATTR_SSL_CA') ) { $options[\PDO::MYSQL_ATTR_SSL_CA] = (string) $databaseConfig['ssl_ca']; } if (array_key_exists('ssl_cipher', $databaseConfig) && !empty($databaseConfig['ssl_cipher']) && defined('\PDO::MYSQL_ATTR_SSL_CIPHER') ) { $options[\PDO::MYSQL_ATTR_SSL_CIPHER] = (string) $databaseConfig['ssl_cipher']; } if (array_key_exists('ssl_verify_server_cert', $databaseConfig) && defined('\PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT') ) { $options[\PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT] = (boolean) $databaseConfig[ 'ssl_verify_server_cert' ]; } try { $this->debugSection('Connecting To Db', ['config' => $databaseConfig, 'options' => $options]); $this->drivers[$databaseKey] = Driver::create($databaseConfig['dsn'], $databaseConfig['user'], $databaseConfig['password'], $options); } catch (\PDOException $e) { $message = $e->getMessage(); if ($message === 'could not find driver') { list ($missingDriver, ) = explode(':', $databaseConfig['dsn'], 2); $message = "could not find $missingDriver driver"; } throw new ModuleException(__CLASS__, $message . ' while creating PDO connection'); } if ($databaseConfig['waitlock']) { $this->_getDriver()->setWaitLock($databaseConfig['waitlock']); } $this->debugSection('Db', 'Connected to ' . $databaseKey . ' ' . $this->drivers[$databaseKey]->getDb()); $this->dbhs[$databaseKey] = $this->drivers[$databaseKey]->getDbh(); } private function disconnect($databaseKey) { $this->debugSection('Db', 'Disconnected from ' . $databaseKey); $this->dbhs[$databaseKey] = null; $this->drivers[$databaseKey] = null; } public function _before(TestInterface $test) { $this->reconnectDatabases(); $this->amConnectedToDatabase(self::DEFAULT_DATABASE); $this->cleanUpDatabases(); $this->populateDatabases('cleanup'); parent::_before($test); } public function _after(TestInterface $test) { $this->removeInsertedForDatabases(); parent::_after($test); } protected function removeInserted($databaseKey = null) { $databaseKey = empty($databaseKey) ? self::DEFAULT_DATABASE : $databaseKey; if (empty($this->insertedRows[$databaseKey])) { return; } foreach (array_reverse($this->insertedRows[$databaseKey]) as $row) { try { $this->_getDriver()->deleteQueryByCriteria($row['table'], $row['primary']); } catch (\Exception $e) { $this->debug("Couldn't delete record " . json_encode($row['primary']) ." from {$row['table']}"); } } $this->insertedRows[$databaseKey] = []; } public function _cleanup($databaseKey = null, $databaseConfig = null) { $databaseKey = empty($databaseKey) ? self::DEFAULT_DATABASE : $databaseKey; $databaseConfig = empty($databaseConfig) ? $this->config : $databaseConfig; if (!$databaseConfig['populate']) { return; } if (!$databaseConfig['cleanup']) { return; } if (isset($this->databasesPopulated[$databaseKey]) && !$this->databasesPopulated[$databaseKey]) { return; } $dbh = $this->dbhs[$databaseKey]; if (!$dbh) { throw new ModuleConfigException( __CLASS__, 'No connection to database. Remove this module from config if you don\'t need database repopulation' ); } try { if (false === $this->shouldCleanup($databaseConfig, $databaseKey)) { return; } $this->drivers[$databaseKey]->cleanup(); $this->databasesPopulated[$databaseKey] = false; } catch (\Exception $e) { throw new ModuleException(__CLASS__, $e->getMessage()); } } /** * @param array $databaseConfig * @param string $databaseKey * @return bool */ protected function shouldCleanup($databaseConfig, $databaseKey) { // If using populator and it's not empty, clean up regardless if (!empty($databaseConfig['populator'])) { return true; } // If no sql dump for $databaseKey or sql dump is empty, don't clean up return !empty($this->databasesSql[$databaseKey]); } public function _isPopulated() { return $this->databasesPopulated[$this->currentDatabase]; } public function _loadDump($databaseKey = null, $databaseConfig = null) { $databaseKey = empty($databaseKey) ? self::DEFAULT_DATABASE : $databaseKey; $databaseConfig = empty($databaseConfig) ? $this->config : $databaseConfig; if ($databaseConfig['populator']) { $this->loadDumpUsingPopulator($databaseKey, $databaseConfig); return; } $this->loadDumpUsingDriver($databaseKey); } protected function loadDumpUsingPopulator($databaseKey, $databaseConfig) { $populator = new DbPopulator($databaseConfig); $this->databasesPopulated[$databaseKey] = $populator->run(); } protected function loadDumpUsingDriver($databaseKey) { if (!isset($this->databasesSql[$databaseKey])) { return; } if (!$this->databasesSql[$databaseKey]) { $this->debugSection('Db', 'No SQL loaded, loading dump skipped'); return; } $this->drivers[$databaseKey]->load($this->databasesSql[$databaseKey]); $this->databasesPopulated[$databaseKey] = true; } /** * Inserts an SQL record into a database. This record will be erased after the test. * * ```php * <?php * $I->haveInDatabase('users', array('name' => 'miles', 'email' => 'miles@davis.com')); * ?> * ``` * * @param string $table * @param array $data * * @return integer $id */ public function haveInDatabase($table, array $data) { $lastInsertId = $this->_insertInDatabase($table, $data); $this->addInsertedRow($table, $data, $lastInsertId); return $lastInsertId; } public function _insertInDatabase($table, array $data) { $query = $this->_getDriver()->insert($table, $data); $parameters = array_values($data); $this->debugSection('Query', $query); $this->debugSection('Parameters', $parameters); $this->_getDriver()->executeQuery($query, $parameters); try { $lastInsertId = (int)$this->_getDriver()->lastInsertId($table); } catch (\PDOException $e) { // ignore errors due to uncommon DB structure, // such as tables without _id_seq in PGSQL $lastInsertId = 0; $this->debugSection('DB error', $e->getMessage()); } return $lastInsertId; } private function addInsertedRow($table, array $row, $id) { $primaryKey = $this->_getDriver()->getPrimaryKey($table); $primary = []; if ($primaryKey) { if ($id && count($primaryKey) === 1) { $primary [$primaryKey[0]] = $id; } else { foreach ($primaryKey as $column) { if (isset($row[$column])) { $primary[$column] = $row[$column]; } else { throw new \InvalidArgumentException( 'Primary key field ' . $column . ' is not set for table ' . $table ); } } } } else { $primary = $row; } $this->insertedRows[$this->currentDatabase][] = [ 'table' => $table, 'primary' => $primary, ]; } public function seeInDatabase($table, $criteria = []) { $res = $this->countInDatabase($table, $criteria); $this->assertGreaterThan( 0, $res, 'No matching records found for criteria ' . json_encode($criteria) . ' in table ' . $table ); } /** * Asserts that the given number of records were found in the database. * * ```php * <?php * $I->seeNumRecords(1, 'users', ['name' => 'davert']) * ?> * ``` * * @param int $expectedNumber Expected number * @param string $table Table name * @param array $criteria Search criteria [Optional] */ public function seeNumRecords($expectedNumber, $table, array $criteria = []) { $actualNumber = $this->countInDatabase($table, $criteria); $this->assertEquals( $expectedNumber, $actualNumber, sprintf( 'The number of found rows (%d) does not match expected number %d for criteria %s in table %s', $actualNumber, $expectedNumber, json_encode($criteria), $table ) ); } public function dontSeeInDatabase($table, $criteria = []) { $count = $this->countInDatabase($table, $criteria); $this->assertLessThan( 1, $count, 'Unexpectedly found matching records for criteria ' . json_encode($criteria) . ' in table ' . $table ); } /** * Count rows in a database * * @param string $table Table name * @param array $criteria Search criteria [Optional] * * @return int */ protected function countInDatabase($table, array $criteria = []) { return (int) $this->proceedSeeInDatabase($table, 'count(*)', $criteria); } /** * Fetches all values from the column in database. * Provide table name, desired column and criteria. * * @param string $table * @param string $column * @param array $criteria * * @return mixed */ protected function proceedSeeInDatabase($table, $column, $criteria) { $query = $this->_getDriver()->select($column, $table, $criteria); $parameters = array_values($criteria); $this->debugSection('Query', $query); if (!empty($parameters)) { $this->debugSection('Parameters', $parameters); } $sth = $this->_getDriver()->executeQuery($query, $parameters); return $sth->fetchColumn(); } /** * Fetches all values from the column in database. * Provide table name, desired column and criteria. * * ``` php * <?php * $mails = $I->grabColumnFromDatabase('users', 'email', array('name' => 'RebOOter')); * ``` * * @param string $table * @param string $column * @param array $criteria * * @return array */ public function grabColumnFromDatabase($table, $column, array $criteria = []) { $query = $this->_getDriver()->select($column, $table, $criteria); $parameters = array_values($criteria); $this->debugSection('Query', $query); $this->debugSection('Parameters', $parameters); $sth = $this->_getDriver()->executeQuery($query, $parameters); return $sth->fetchAll(\PDO::FETCH_COLUMN, 0); } /** * Fetches all values from the column in database. * Provide table name, desired column and criteria. * * ``` php * <?php * $mails = $I->grabFromDatabase('users', 'email', array('name' => 'RebOOter')); * ``` * * @param string $table * @param string $column * @param array $criteria * * @return mixed */ public function grabFromDatabase($table, $column, $criteria = []) { return $this->proceedSeeInDatabase($table, $column, $criteria); } /** * Returns the number of rows in a database * * @param string $table Table name * @param array $criteria Search criteria [Optional] * * @return int */ public function grabNumRecords($table, array $criteria = []) { return $this->countInDatabase($table, $criteria); } /** * Update an SQL record into a database. * * ```php * <?php * $I->updateInDatabase('users', array('isAdmin' => true), array('email' => 'miles@davis.com')); * ?> * ``` * * @param string $table * @param array $data * @param array $criteria */ public function updateInDatabase($table, array $data, array $criteria = []) { $query = $this->_getDriver()->update($table, $data, $criteria); $parameters = array_merge(array_values($data), array_values($criteria)); $this->debugSection('Query', $query); if (!empty($parameters)) { $this->debugSection('Parameters', $parameters); } $this->_getDriver()->executeQuery($query, $parameters); } } <?php namespace Codeception\Module; use Codeception\Lib\Interfaces\DataMapper; use Codeception\Module as CodeceptionModule; use Codeception\Exception\ModuleConfigException; use Codeception\Lib\Interfaces\DependsOnModule; use Codeception\Lib\Interfaces\DoctrineProvider; use Codeception\TestInterface; use Codeception\Util\Stub; /** * Access the database using [Doctrine2 ORM](http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/). * * When used with Zend Framework 2 or Symfony2, Doctrine's Entity Manager is automatically retrieved from Service Locator. * Set up your `functional.suite.yml` like this: * * ``` * modules: * enabled: * - Symfony # 'ZF2' or 'Symfony' * - Doctrine2: * depends: Symfony * cleanup: true # All doctrine queries will be wrapped in a transaction, which will be rolled back at the end of each test * ``` * * If you don't use Symfony or Zend Framework, you need to specify a callback function to retrieve the Entity Manager: * * ``` * modules: * enabled: * - Doctrine2: * connection_callback: ['MyDb', 'createEntityManager'] * cleanup: true # All doctrine queries will be wrapped in a transaction, which will be rolled back at the end of each test * * ``` * * This will use static method of `MyDb::createEntityManager()` to establish the Entity Manager. * * By default, the module will wrap everything into a transaction for each test and roll it back afterwards. By doing this * tests will run much faster and will be isolated from each other. * * ## Status * * * Maintainer: **davert** * * Stability: **stable** * * Contact: codecept@davert.mail.ua * * ## Config * * ## Public Properties * * * `em` - Entity Manager */ class Doctrine2 extends CodeceptionModule implements DependsOnModule, DataMapper { protected $config = [ 'cleanup' => true, 'connection_callback' => false, 'depends' => null ]; protected $dependencyMessage = <<<EOF Provide connection_callback function to establish database connection and get Entity Manager: modules: enabled: - Doctrine2: connection_callback: [My\ConnectionClass, getEntityManager] Or set a dependent module, which can be either Symfony or ZF2 to get EM from service locator: modules: enabled: - Doctrine2: depends: Symfony EOF; /** * @var \Doctrine\ORM\EntityManagerInterface */ public $em = null; /** * @var \Codeception\Lib\Interfaces\DoctrineProvider */ private $dependentModule; public function _depends() { if ($this->config['connection_callback']) { return []; } return ['Codeception\Lib\Interfaces\DoctrineProvider' => $this->dependencyMessage]; } public function _inject(DoctrineProvider $dependentModule = null) { $this->dependentModule = $dependentModule; } public function _beforeSuite($settings = []) { $this->retrieveEntityManager(); } public function _before(TestInterface $test) { $this->retrieveEntityManager(); if ($this->config['cleanup']) { if ($this->em->getConnection()->isTransactionActive()) { try { while ($this->em->getConnection()->getTransactionNestingLevel() > 0) { $this->em->getConnection()->rollback(); } $this->debugSection('Database', 'Transaction cancelled; all changes reverted.'); } catch (\PDOException $e) { } } $this->em->getConnection()->beginTransaction(); $this->debugSection('Database', 'Transaction started'); } } /** * @throws ModuleConfigException */ public function onReconfigure() { if (!$this->em instanceof \Doctrine\ORM\EntityManagerInterface) { return; } if ($this->config['cleanup'] && $this->em->getConnection()->isTransactionActive()) { try { $this->em->getConnection()->rollback(); $this->debugSection('Database', 'Transaction cancelled; all changes reverted.'); } catch (\PDOException $e) { } } $this->clean(); $this->em->getConnection()->close(); $this->retrieveEntityManager(); if ($this->config['cleanup']) { if ($this->em->getConnection()->isTransactionActive()) { try { while ($this->em->getConnection()->getTransactionNestingLevel() > 0) { $this->em->getConnection()->rollback(); } $this->debugSection('Database', 'Transaction cancelled; all changes reverted.'); } catch (\PDOException $e) { } } $this->em->getConnection()->beginTransaction(); $this->debugSection('Database', 'Transaction started'); } } protected function retrieveEntityManager() { if ($this->dependentModule) { $this->em = $this->dependentModule->_getEntityManager(); } else { if (is_callable($this->config['connection_callback'])) { $this->em = call_user_func($this->config['connection_callback']); } } if (!$this->em) { throw new ModuleConfigException( __CLASS__, "EntityManager can't be obtained.\n \n" . "Please specify either `connection_callback` config option\n" . "with callable which will return instance of EntityManager or\n" . "pass a dependent module which are Symfony or ZF2\n" . "to connect to Doctrine using Dependency Injection Container" ); } if (!($this->em instanceof \Doctrine\ORM\EntityManagerInterface)) { throw new ModuleConfigException( __CLASS__, "Connection object is not an instance of \\Doctrine\\ORM\\EntityManagerInterface.\n" . "Use `connection_callback` or dependent framework modules to specify one" ); } $this->em->getConnection()->connect(); } public function _after(TestInterface $test) { if (!$this->em instanceof \Doctrine\ORM\EntityManagerInterface) { return; } if ($this->config['cleanup'] && $this->em->getConnection()->isTransactionActive()) { try { while ($this->em->getConnection()->getTransactionNestingLevel() > 0) { $this->em->getConnection()->rollback(); } $this->debugSection('Database', 'Transaction cancelled; all changes reverted.'); } catch (\PDOException $e) { } } $this->clean(); $this->em->getConnection()->close(); } protected function clean() { $em = $this->em; $reflectedEm = new \ReflectionClass($em); if ($reflectedEm->hasProperty('repositories')) { $property = $reflectedEm->getProperty('repositories'); $property->setAccessible(true); $property->setValue($em, []); } $this->em->clear(); } /** * Performs $em->flush(); */ public function flushToDatabase() { $this->em->flush(); } /** * Adds entity to repository and flushes. You can redefine it's properties with the second parameter. * * Example: * * ``` php * <?php * $I->persistEntity(new \Entity\User, array('name' => 'Miles')); * $I->persistEntity($user, array('name' => 'Miles')); * ``` * * @param $obj * @param array $values */ public function persistEntity($obj, $values = []) { if ($values) { $reflectedObj = new \ReflectionClass($obj); foreach ($values as $key => $val) { $property = $reflectedObj->getProperty($key); $property->setAccessible(true); $property->setValue($obj, $val); } } $this->em->persist($obj); $this->em->flush(); } /** * Mocks the repository. * * With this action you can redefine any method of any repository. * Please, note: this fake repositories will be accessible through entity manager till the end of test. * * Example: * * ``` php * <?php * * $I->haveFakeRepository('Entity\User', array('findByUsername' => function($username) { return null; })); * * ``` * * This creates a stub class for Entity\User repository with redefined method findByUsername, * which will always return the NULL value. * * @param $classname * @param array $methods */ public function haveFakeRepository($classname, $methods = []) { $em = $this->em; $metadata = $em->getMetadataFactory()->getMetadataFor($classname); $customRepositoryClassName = $metadata->customRepositoryClassName; if (!$customRepositoryClassName) { $customRepositoryClassName = '\Doctrine\ORM\EntityRepository'; } $mock = Stub::make( $customRepositoryClassName, array_merge( [ '_entityName' => $metadata->name, '_em' => $em, '_class' => $metadata ], $methods ) ); $em->clear(); $reflectedEm = new \ReflectionClass($em); if ($reflectedEm->hasProperty('repositories')) { //Support doctrine versions before 2.4.0 $property = $reflectedEm->getProperty('repositories'); $property->setAccessible(true); $property->setValue($em, array_merge($property->getValue($em), [$classname => $mock])); } elseif ($reflectedEm->hasProperty('repositoryFactory')) { //For doctrine 2.4.0+ versions $repositoryFactoryProperty = $reflectedEm->getProperty('repositoryFactory'); $repositoryFactoryProperty->setAccessible(true); $repositoryFactory = $repositoryFactoryProperty->getValue($em); $reflectedRepositoryFactory = new \ReflectionClass($repositoryFactory); if ($reflectedRepositoryFactory->hasProperty('repositoryList')) { $repositoryListProperty = $reflectedRepositoryFactory->getProperty('repositoryList'); $repositoryListProperty->setAccessible(true); $repositoryListProperty->setValue( $repositoryFactory, [$classname => $mock] ); $repositoryFactoryProperty->setValue($em, $repositoryFactory); } else { $this->debugSection( 'Warning', 'Repository can\'t be mocked, the EventManager\'s repositoryFactory doesn\'t have "repositoryList" property' ); } } else { $this->debugSection( 'Warning', 'Repository can\'t be mocked, the EventManager class doesn\'t have "repositoryFactory" or "repositories" property' ); } } /** * Persists record into repository. * This method creates an entity, and sets its properties directly (via reflection). * Setters of entity won't be executed, but you can create almost any entity and save it to database. * Returns id using `getId` of newly created entity. * * ```php * $I->haveInRepository('Entity\User', array('name' => 'davert')); * ``` */ public function haveInRepository($entity, array $data) { $reflectedEntity = new \ReflectionClass($entity); $entityObject = $reflectedEntity->newInstance(); foreach ($reflectedEntity->getProperties() as $property) { /** @var $property \ReflectionProperty */ if (!isset($data[$property->name])) { continue; } $property->setAccessible(true); $property->setValue($entityObject, $data[$property->name]); } $this->em->persist($entityObject); $this->em->flush(); if (method_exists($entityObject, 'getId')) { $id = $entityObject->getId(); $this->debug("$entity entity created with id:$id"); return $id; } } /** * Flushes changes to database, and executes a query with parameters defined in an array. * You can use entity associations to build complex queries. * * Example: * * ``` php * <?php * $I->seeInRepository('AppBundle:User', array('name' => 'davert')); * $I->seeInRepository('User', array('name' => 'davert', 'Company' => array('name' => 'Codegyre'))); * $I->seeInRepository('Client', array('User' => array('Company' => array('name' => 'Codegyre'))); * ?> * ``` * * Fails if record for given criteria can\'t be found, * * @param $entity * @param array $params */ public function seeInRepository($entity, $params = []) { $res = $this->proceedSeeInRepository($entity, $params); $this->assert($res); } /** * Flushes changes to database and performs `findOneBy()` call for current repository. * * @param $entity * @param array $params */ public function dontSeeInRepository($entity, $params = []) { $res = $this->proceedSeeInRepository($entity, $params); $this->assertNot($res); } protected function proceedSeeInRepository($entity, $params = []) { // we need to store to database... $this->em->flush(); $data = $this->em->getClassMetadata($entity); $qb = $this->em->getRepository($entity)->createQueryBuilder('s'); $this->buildAssociationQuery($qb, $entity, 's', $params); $this->debug($qb->getDQL()); $res = $qb->getQuery()->getArrayResult(); return ['True', (count($res) > 0), "$entity with " . json_encode($params)]; } /** * Selects field value from repository. * It builds query based on array of parameters. * You can use entity associations to build complex queries. * * Example: * * ``` php * <?php * $email = $I->grabFromRepository('User', 'email', array('name' => 'davert')); * ?> * ``` * * @version 1.1 * @param $entity * @param $field * @param array $params * @return array */ public function grabFromRepository($entity, $field, $params = []) { // we need to store to database... $this->em->flush(); $data = $this->em->getClassMetadata($entity); $qb = $this->em->getRepository($entity)->createQueryBuilder('s'); $qb->select('s.' . $field); $this->buildAssociationQuery($qb, $entity, 's', $params); $this->debug($qb->getDQL()); return $qb->getQuery()->getSingleScalarResult(); } /** * Selects entities from repository. * It builds query based on array of parameters. * You can use entity associations to build complex queries. * * Example: * * ``` php * <?php * $users = $I->grabEntitiesFromRepository('AppBundle:User', array('name' => 'davert')); * ?> * ``` * * @version 1.1 * @param $entity * @param array $params. For `IS NULL`, use `array('field'=>null)` * @return array */ public function grabEntitiesFromRepository($entity, $params = []) { // we need to store to database... $this->em->flush(); $data = $this->em->getClassMetadata($entity); $qb = $this->em->getRepository($entity)->createQueryBuilder('s'); $qb->select('s'); $this->buildAssociationQuery($qb, $entity, 's', $params); $this->debug($qb->getDQL()); return $qb->getQuery()->getResult(); } /** * Selects a single entity from repository. * It builds query based on array of parameters. * You can use entity associations to build complex queries. * * Example: * * ``` php * <?php * $user = $I->grabEntityFromRepository('User', array('id' => '1234')); * ?> * ``` * * @version 1.1 * @param $entity * @param array $params. For `IS NULL`, use `array('field'=>null)` * @return object */ public function grabEntityFromRepository($entity, $params = []) { // we need to store to database... $this->em->flush(); $data = $this->em->getClassMetadata($entity); $qb = $this->em->getRepository($entity)->createQueryBuilder('s'); $qb->select('s'); $this->buildAssociationQuery($qb, $entity, 's', $params); $this->debug($qb->getDQL()); return $qb->getQuery()->getSingleResult(); } /** * It's Fuckin Recursive! * * @param $qb * @param $assoc * @param $alias * @param $params */ protected function buildAssociationQuery($qb, $assoc, $alias, $params) { $data = $this->em->getClassMetadata($assoc); foreach ($params as $key => $val) { if (isset($data->associationMappings)) { if (array_key_exists($key, $data->associationMappings)) { $map = $data->associationMappings[$key]; if (is_array($val)) { $qb->innerJoin("$alias.$key", "${alias}__$key"); $this->buildAssociationQuery($qb, $map['targetEntity'], "${alias}__$key", $val); continue; } } } if ($val === null) { $qb->andWhere("$alias.$key IS NULL"); } else { $paramname = str_replace(".", "", "${alias}_$key"); $qb->andWhere("$alias.$key = :$paramname"); $qb->setParameter($paramname, $val); } } } public function _getEntityManager() { if (is_null($this->em)) { $this->retrieveEntityManager(); } return $this->em; } } <?php namespace Codeception\Module; use Codeception\Util\FileSystem as Util; use Symfony\Component\Finder\Finder; use Codeception\Module as CodeceptionModule; use Codeception\TestInterface; use Codeception\Configuration; /** * Module for testing local filesystem. * Fork it to extend the module for FTP, Amazon S3, others. * * ## Status * * * Maintainer: **davert** * * Stability: **stable** * * Contact: codecept@davert.mail.ua * * Module was developed to test Codeception itself. */ class Filesystem extends CodeceptionModule { protected $file = null; protected $filepath = null; protected $path = ''; public function _before(TestInterface $test) { $this->path = Configuration::projectDir(); } /** * Enters a directory In local filesystem. * Project root directory is used by default * * @param string $path */ public function amInPath($path) { chdir($this->path = $this->absolutizePath($path) . DIRECTORY_SEPARATOR); $this->debug('Moved to ' . getcwd()); } /** * @param string $path * @return string */ protected function absolutizePath($path) { // *nix way if (strpos($path, '/') === 0) { return $path; } // windows if (strpos($path, ':\\') === 1) { return $path; } return $this->path . $path; } /** * Opens a file and stores it's content. * * Usage: * * ``` php * <?php * $I->openFile('composer.json'); * $I->seeInThisFile('codeception/codeception'); * ?> * ``` * * @param string $filename */ public function openFile($filename) { $this->file = file_get_contents($this->absolutizePath($filename)); $this->filepath = $filename; } /** * Deletes a file * * ``` php * <?php * $I->deleteFile('composer.lock'); * ?> * ``` * * @param string $filename */ public function deleteFile($filename) { if (!file_exists($this->absolutizePath($filename))) { \PHPUnit\Framework\Assert::fail('file not found'); } unlink($this->absolutizePath($filename)); } /** * Deletes directory with all subdirectories * * ``` php * <?php * $I->deleteDir('vendor'); * ?> * ``` * * @param string $dirname */ public function deleteDir($dirname) { $dir = $this->absolutizePath($dirname); Util::deleteDir($dir); } /** * Copies directory with all contents * * ``` php * <?php * $I->copyDir('vendor','old_vendor'); * ?> * ``` * * @param string $src * @param string $dst */ public function copyDir($src, $dst) { Util::copyDir($src, $dst); } /** * Checks If opened file has `text` in it. * * Usage: * * ``` php * <?php * $I->openFile('composer.json'); * $I->seeInThisFile('codeception/codeception'); * ?> * ``` * * @param string $text */ public function seeInThisFile($text) { $this->assertStringContainsString($text, $this->file, "No text '$text' in currently opened file"); } /** * Checks If opened file has the `number` of new lines. * * Usage: * * ``` php * <?php * $I->openFile('composer.json'); * $I->seeNumberNewLines(5); * ?> * ``` * * @param int $number New lines */ public function seeNumberNewLines($number) { $lines = preg_split('/\n|\r/', $this->file); $this->assertTrue( (int) $number === count($lines), "The number of new lines does not match with $number" ); } /** * Checks that contents of currently opened file matches $regex * * @param string $regex */ public function seeThisFileMatches($regex) { $this->assertRegExp($regex, $this->file, "Contents of currently opened file does not match '$regex'"); } /** * Checks the strict matching of file contents. * Unlike `seeInThisFile` will fail if file has something more than expected lines. * Better to use with HEREDOC strings. * Matching is done after removing "\r" chars from file content. * * ``` php * <?php * $I->openFile('process.pid'); * $I->seeFileContentsEqual('3192'); * ?> * ``` * * @param string $text */ public function seeFileContentsEqual($text) { $file = str_replace("\r", '', $this->file); \PHPUnit\Framework\Assert::assertEquals($text, $file); } /** * Checks If opened file doesn't contain `text` in it * * ``` php * <?php * $I->openFile('composer.json'); * $I->dontSeeInThisFile('codeception/codeception'); * ?> * ``` * * @param string $text */ public function dontSeeInThisFile($text) { $this->assertStringNotContainsString($text, $this->file, "Found text '$text' in currently opened file"); } /** * Deletes a file */ public function deleteThisFile() { $this->deleteFile($this->filepath); } /** * Checks if file exists in path. * Opens a file when it's exists * * ``` php * <?php * $I->seeFileFound('UserModel.php','app/models'); * ?> * ``` * * @param string $filename * @param string $path */ public function seeFileFound($filename, $path = '') { if ($path === '' && file_exists($filename)) { $this->openFile($filename); \PHPUnit\Framework\Assert::assertFileExists($filename); return; } $found = $this->findFileInPath($filename, $path); if ($found === false) { $this->fail("File \"$filename\" not found at \"$path\""); } $this->openFile($found); \PHPUnit\Framework\Assert::assertFileExists($found); } /** * Checks if file does not exist in path * * @param string $filename * @param string $path */ public function dontSeeFileFound($filename, $path = '') { if ($path === '') { \PHPUnit\Framework\Assert::assertFileNotExists($filename); return; } $found = $this->findFileInPath($filename, $path); if ($found === false) { //this line keeps a count of assertions correct \PHPUnit\Framework\Assert::assertTrue(true); return; } \PHPUnit\Framework\Assert::assertFileNotExists($found); } /** * Finds the first matching file * * @param string $filename * @param string $path * @throws \PHPUnit\Framework\AssertionFailedError When path does not exist * @return string|false Path to the first matching file */ private function findFileInPath($filename, $path) { $path = $this->absolutizePath($path); if (!file_exists($path)) { $this->fail("Directory does not exist: $path"); } $files = Finder::create()->files()->name($filename)->in($path); if ($files->count() === 0) { return false; } foreach ($files as $file) { return $file->getRealPath(); } } /** * Erases directory contents * * ``` php * <?php * $I->cleanDir('logs'); * ?> * ``` * * @param string $dirname */ public function cleanDir($dirname) { $path = $this->absolutizePath($dirname); Util::doEmptyDir($path); } /** * Saves contents to file * * @param string $filename * @param string $contents */ public function writeToFile($filename, $contents) { file_put_contents($filename, $contents); } } <?php namespace Codeception\Module; use Codeception\TestInterface; use Codeception\Exception\ModuleException; /** * * Works with SFTP/FTP servers. * * In order to test the contents of a specific file stored on any remote FTP/SFTP system * this module downloads a temporary file to the local system. The temporary directory is * defined by default as ```tests/_data``` to specify a different directory set the tmp config * option to your chosen path. * * Don't forget to create the folder and ensure its writable. * * Supported and tested FTP types are: * * * FTP * * SFTP * * Connection uses php build in FTP client for FTP, * connection to SFTP uses [phpseclib](http://phpseclib.sourceforge.net/) pulled in using composer. * * For SFTP, add [phpseclib](http://phpseclib.sourceforge.net/) to require list. * ``` * "require": { * "phpseclib/phpseclib": "^2.0.14" * } * ``` * * ## Status * * * Stability: * - FTP: **stable** * - SFTP: **stable** * * ## Config * * * type: ftp - type of connection ftp/sftp (defaults to ftp). * * host *required* - hostname/ip address of the ftp server. * * port: 21 - port number for the ftp server * * timeout: 90 - timeout settings for connecting the ftp server. * * user: anonymous - user to access ftp server, defaults to anonymous authentication. * * password - password, defaults to empty for anonymous. * * key - path to RSA key for sftp. * * tmp - path to local directory for storing tmp files. * * passive: true - Turns on or off passive mode (FTP only) * * cleanup: true - remove tmp files from local directory on completion. * * ### Example * #### Example (FTP) * * modules: * enabled: [FTP] * config: * FTP: * type: ftp * host: '127.0.0.1' * port: 21 * timeout: 120 * user: 'root' * password: 'root' * key: ~/.ssh/id_rsa * tmp: 'tests/_data/ftp' * passive: true * cleanup: false * * #### Example (SFTP) * * modules: * enabled: [FTP] * config: * FTP: * type: sftp * host: '127.0.0.1' * port: 22 * timeout: 120 * user: 'root' * password: 'root' * key: '' * tmp: 'tests/_data/ftp' * cleanup: false * * * This module extends the Filesystem module, file contents methods are inherited from this module. */ class FTP extends Filesystem { /** * FTP/SFTP connection handler */ protected $ftp = null; /** * Configuration options and default settings * * @var array */ protected $config = [ 'type' => 'ftp', 'port' => 21, 'timeout' => 90, 'user' => 'anonymous', 'password' => '', 'key' => '', 'tmp' => 'tests/_data', 'passive' => false, 'cleanup' => true ]; /** * Required configuration fields * * @var array */ protected $requiredFields = ['host']; // ----------- SETUP METHODS BELOW HERE -------------------------// /** * Setup connection and login with config settings * * @param \Codeception\TestInterface $test */ public function _before(TestInterface $test) { // Login using config settings $this->loginAs($this->config['user'], $this->config['password']); } /** * Close the FTP connection & Clear up */ public function _after(TestInterface $test) { $this->_closeConnection(); // Clean up temp files if ($this->config['cleanup']) { if (file_exists($this->config['tmp'] . '/ftp_data_file.tmp')) { unlink($this->config['tmp'] . '/ftp_data_file.tmp'); } } } /** * Change the logged in user mid-way through your test, this closes the * current connection to the server and initialises and new connection. * * On initiation of this modules you are automatically logged into * the server using the specified config options or defaulted * to anonymous user if not provided. * * ``` php * <?php * $I->loginAs('user','password'); * ?> * ``` * * @param String $user * @param String $password */ public function loginAs($user = 'anonymous', $password = '') { $this->_openConnection($user, $password); // Create new connection and login. } /** * Enters a directory on the ftp system - FTP root directory is used by default * * @param $path */ public function amInPath($path) { $this->_changeDirectory($this->path = $this->absolutizePath($path) . ($path == '/' ? '' : DIRECTORY_SEPARATOR)); $this->debug('Moved to ' . $this->path); } /** * Resolve path * * @param $path * @return string */ protected function absolutizePath($path) { if (strpos($path, '/') === 0) { return $path; } return $this->path . $path; } // ----------- SEARCH METHODS BELOW HERE ------------------------// /** * Checks if file exists in path on the remote FTP/SFTP system. * DOES NOT OPEN the file when it's exists * * ``` php * <?php * $I->seeFileFound('UserModel.php','app/models'); * ?> * ``` * * @param $filename * @param string $path */ public function seeFileFound($filename, $path = '') { $files = $this->grabFileList($path); $this->debug("see file: {$filename}"); $this->assertContains($filename, $files, "file {$filename} not found in {$path}"); } /** * Checks if file exists in path on the remote FTP/SFTP system, using regular expression as filename. * DOES NOT OPEN the file when it's exists * * ``` php * <?php * $I->seeFileFoundMatches('/^UserModel_([0-9]{6}).php$/','app/models'); * ?> * ``` * * @param $regex * @param string $path */ public function seeFileFoundMatches($regex, $path = '') { foreach ($this->grabFileList($path) as $filename) { preg_match($regex, $filename, $matches); if (!empty($matches)) { $this->debug("file '{$filename}' matches '{$regex}'"); return; } } $this->fail("no file matches found for '{$regex}'"); } /** * Checks if file does not exist in path on the remote FTP/SFTP system * * @param $filename * @param string $path */ public function dontSeeFileFound($filename, $path = '') { $files = $this->grabFileList($path); $this->debug("don't see file: {$filename}"); $this->assertNotContains($filename, $files); } /** * Checks if file does not exist in path on the remote FTP/SFTP system, using regular expression as filename. * DOES NOT OPEN the file when it's exists * * @param $regex * @param string $path */ public function dontSeeFileFoundMatches($regex, $path = '') { foreach ($this->grabFileList($path) as $filename) { preg_match($regex, $filename, $matches); if (!empty($matches)) { $this->fail("file matches found for {$regex}"); } } $this->assertTrue(true); $this->debug("no files match '{$regex}'"); } // ----------- UTILITY METHODS BELOW HERE -------------------------// /** * Opens a file (downloads from the remote FTP/SFTP system to a tmp directory for processing) * and stores it's content. * * Usage: * * ``` php * <?php * $I->openFile('composer.json'); * $I->seeInThisFile('codeception/codeception'); * ?> * ``` * * @param $filename */ public function openFile($filename) { $this->_openFile($this->absolutizePath($filename)); } /** * Saves contents to tmp file and uploads the FTP/SFTP system. * Overwrites current file on server if exists. * * ``` php * <?php * $I->writeToFile('composer.json', 'some data here'); * ?> * ``` * * @param $filename * @param $contents */ public function writeToFile($filename, $contents) { $this->_writeToFile($this->absolutizePath($filename), $contents); } /** * Create a directory on the server * * ``` php * <?php * $I->makeDir('vendor'); * ?> * ``` * * @param $dirname */ public function makeDir($dirname) { $this->makeDirectory($this->absolutizePath($dirname)); } /** * Currently not supported in this module, overwrite inherited method * * @param $src * @param $dst */ public function copyDir($src, $dst) { $this->fail('copyDir() currently unsupported by FTP module'); } /** * Rename/Move file on the FTP/SFTP server * * ``` php * <?php * $I->renameFile('composer.lock', 'composer_old.lock'); * ?> * ``` * * @param $filename * @param $rename */ public function renameFile($filename, $rename) { $this->renameDirectory($this->absolutizePath($filename), $this->absolutizePath($rename)); } /** * Rename/Move directory on the FTP/SFTP server * * ``` php * <?php * $I->renameDir('vendor', 'vendor_old'); * ?> * ``` * * @param $dirname * @param $rename */ public function renameDir($dirname, $rename) { $this->renameDirectory($this->absolutizePath($dirname), $this->absolutizePath($rename)); } /** * Deletes a file on the remote FTP/SFTP system * * ``` php * <?php * $I->deleteFile('composer.lock'); * ?> * ``` * * @param $filename */ public function deleteFile($filename) { $this->delete($this->absolutizePath($filename)); } /** * Deletes directory with all subdirectories on the remote FTP/SFTP server * * ``` php * <?php * $I->deleteDir('vendor'); * ?> * ``` * * @param $dirname */ public function deleteDir($dirname) { $this->delete($this->absolutizePath($dirname)); } /** * Erases directory contents on the FTP/SFTP server * * ``` php * <?php * $I->cleanDir('logs'); * ?> * ``` * * @param $dirname */ public function cleanDir($dirname) { $this->clearDirectory($this->absolutizePath($dirname)); } // ----------- GRABBER METHODS BELOW HERE -----------------------// /** * Grabber method for returning file/folders listing in an array * * ```php * <?php * $files = $I->grabFileList(); * $count = $I->grabFileList('TEST', false); // Include . .. .thumbs.db * ?> * ``` * * @param string $path * @param bool $ignore - suppress '.', '..' and '.thumbs.db' * @return array */ public function grabFileList($path = '', $ignore = true) { $absolutize_path = $this->absolutizePath($path) . ($path != '' && substr($path, -1) != '/' ? DIRECTORY_SEPARATOR : ''); $files = $this->_listFiles($absolutize_path); $display_files = []; if (is_array($files) && !empty($files)) { $this->debug('File List:'); foreach ($files as &$file) { if (strtolower($file) != '.' && strtolower($file) != '..' && strtolower($file) != 'thumbs.db' ) { // Ignore '.', '..' and 'thumbs.db' // Replace full path from file listings if returned in listing $file = str_replace( $absolutize_path, '', $file ); $display_files[] = $file; $this->debug(' - ' . $file); } } return $ignore ? $display_files : $files; } $this->debug("File List: <empty>"); return []; } /** * Grabber method for returning file/folders count in directory * * ```php * <?php * $count = $I->grabFileCount(); * $count = $I->grabFileCount('TEST', false); // Include . .. .thumbs.db * ?> * ``` * * @param string $path * @param bool $ignore - suppress '.', '..' and '.thumbs.db' * @return int */ public function grabFileCount($path = '', $ignore = true) { $count = count($this->grabFileList($path, $ignore)); $this->debug("File Count: {$count}"); return $count; } /** * Grabber method to return file size * * ```php * <?php * $size = $I->grabFileSize('test.txt'); * ?> * ``` * * @param $filename * @return bool */ public function grabFileSize($filename) { $fileSize = $this->size($filename); $this->debug("{$filename} has a file size of {$fileSize}"); return $fileSize; } /** * Grabber method to return last modified timestamp * * ```php * <?php * $time = $I->grabFileModified('test.txt'); * ?> * ``` * * @param $filename * @return bool */ public function grabFileModified($filename) { $time = $this->modified($filename); $this->debug("{$filename} was last modified at {$time}"); return $time; } /** * Grabber method to return current working directory * * ```php * <?php * $pwd = $I->grabDirectory(); * ?> * ``` * * @return string */ public function grabDirectory() { $pwd = $this->_directory(); $this->debug("PWD: {$pwd}"); return $pwd; } // ----------- SERVER CONNECTION METHODS BELOW HERE -------------// /** * Open a new FTP/SFTP connection and authenticate user. * * @param string $user * @param string $password */ private function _openConnection($user = 'anonymous', $password = '') { $this->_closeConnection(); // Close connection if already open if ($this->isSFTP()) { $this->sftpConnect($user, $password); } else { $this->ftpConnect($user, $password); } $pwd = $this->grabDirectory(); $this->path = $pwd . ($pwd == '/' ? '' : DIRECTORY_SEPARATOR); } /** * Close open FTP/SFTP connection */ private function _closeConnection() { if (!$this->ftp) { return; } if (!$this->isSFTP()) { ftp_close($this->ftp); $this->ftp = null; } } /** * Get the file listing for FTP/SFTP connection * * @param String $path * @return array */ private function _listFiles($path) { if ($this->isSFTP()) { $files = @$this->ftp->nlist($path); } else { $files = @ftp_nlist($this->ftp, $path); } if ($files === false) { $this->fail("couldn't list files"); } return $files; } /** * Get the current directory for the FTP/SFTP connection * * @return string */ private function _directory() { if ($this->isSFTP()) { // == DIRECTORY_SEPARATOR ? '' : $pwd; $pwd = @$this->ftp->pwd(); } else { $pwd = @ftp_pwd($this->ftp); } if (!$pwd) { $this->fail("couldn't get current directory"); } return $pwd; } /** * Change the working directory on the FTP/SFTP server * * @param $path */ private function _changeDirectory($path) { if ($this->isSFTP()) { $changed = @$this->ftp->chdir($path); } else { $changed = @ftp_chdir($this->ftp, $path); } if (!$changed) { $this->fail("couldn't change directory {$path}"); } } /** * Download remote file to local tmp directory and open contents. * * @param $filename */ private function _openFile($filename) { // Check local tmp directory if (!is_dir($this->config['tmp']) || !is_writeable($this->config['tmp'])) { $this->fail('tmp directory not found or is not writable'); } // Download file to local tmp directory $tmp_file = $this->config['tmp'] . "/ftp_data_file.tmp"; if ($this->isSFTP()) { $downloaded = @$this->ftp->get($filename, $tmp_file); } else { $downloaded = @ftp_get($this->ftp, $tmp_file, $filename, FTP_BINARY); } if (!$downloaded) { $this->fail('failed to download file to tmp directory'); } // Open file content to variable if ($this->file = file_get_contents($tmp_file)) { $this->filepath = $filename; } else { $this->fail('failed to open tmp file'); } } /** * Write data to local tmp file and upload to server * * @param $filename * @param $contents */ private function _writeToFile($filename, $contents) { // Check local tmp directory if (!is_dir($this->config['tmp']) || !is_writeable($this->config['tmp'])) { $this->fail('tmp directory not found or is not writable'); } // Build temp file $tmp_file = $this->config['tmp'] . "/ftp_data_file.tmp"; file_put_contents($tmp_file, $contents); // Update variables $this->filepath = $filename; $this->file = $contents; // Upload the file to server if ($this->isSFTP()) { $flag = defined('NET_SFTP_LOCAL_FILE') ? NET_SFTP_LOCAL_FILE : \phpseclib\Net\SFTP::SOURCE_LOCAL_FILE; $uploaded = @$this->ftp->put($filename, $tmp_file, $flag); } else { $uploaded = ftp_put($this->ftp, $filename, $tmp_file, FTP_BINARY); } if (!$uploaded) { $this->fail('failed to upload file to server'); } } /** * Make new directory on server * * @param $path */ private function makeDirectory($path) { if ($this->isSFTP()) { $created = @$this->ftp->mkdir($path, true); } else { $created = @ftp_mkdir($this->ftp, $path); } if (!$created) { $this->fail("couldn't make directory {$path}"); } $this->debug("Make directory: {$path}"); } /** * Rename/Move directory/file on server * * @param $path * @param $rename */ private function renameDirectory($path, $rename) { if ($this->isSFTP()) { $renamed = @$this->ftp->rename($path, $rename); } else { $renamed = @ftp_rename($this->ftp, $path, $rename); } if (!$renamed) { $this->fail("couldn't rename directory {$path} to {$rename}"); } $this->debug("Renamed directory: {$path} to {$rename}"); } /** * Delete file on server * * @param $filename */ private function delete($filename, $isDir = false) { if ($this->isSFTP()) { $deleted = @$this->ftp->delete($filename, $isDir); } else { $deleted = @$this->ftpDelete($filename); } if (!$deleted) { $this->fail("couldn't delete {$filename}"); } $this->debug("Deleted: {$filename}"); } /** * Function to recursively delete folder, used for PHP FTP build in client. * * @param $directory * @return bool */ private function ftpDelete($directory) { // here we attempt to delete the file/directory if (!(@ftp_rmdir($this->ftp, $directory) || @ftp_delete($this->ftp, $directory))) { // if the attempt to delete fails, get the file listing $filelist = @ftp_nlist($this->ftp, $directory); // loop through the file list and recursively delete the FILE in the list foreach ($filelist as $file) { $this->ftpDelete($file); } // if the file list is empty, delete the DIRECTORY we passed $this->ftpDelete($directory); } return true; } /** * Clear directory on server of all content * * @param $path */ private function clearDirectory($path) { $this->debug("Clear directory: {$path}"); $this->delete($path); $this->makeDirectory($path); } /** * Return the size of a given file * * @param $filename * @return bool */ private function size($filename) { if ($this->isSFTP()) { $size = (int)@$this->ftp->size($filename); } else { $size = @ftp_size($this->ftp, $filename); } if ($size > 0) { return $size; } $this->fail("couldn't get the file size for {$filename}"); } /** * Return the last modified time of a given file * * @param $filename * @return bool */ private function modified($filename) { if ($this->isSFTP()) { $info = @$this->ftp->lstat($filename); if ($info) { return $info['mtime']; } } else { if ($time = @ftp_mdtm($this->ftp, $filename)) { return $time; } } $this->fail("couldn't get the file size for {$filename}"); } /** * @param $user * @param $password */ protected function sftpConnect($user, $password) { if (class_exists('Net_SFTP')) { $this->ftp = new \Net_SFTP($this->config['host'], $this->config['port'], $this->config['timeout']); } elseif (class_exists('phpseclib\Net\SFTP')) { $this->ftp = new \phpseclib\Net\SFTP($this->config['host'], $this->config['port'], $this->config['timeout']); } else { throw new ModuleException('phpseclib/phpseclib library is not installed'); } if ($this->ftp === false) { $this->ftp = null; $this->fail('failed to connect to ftp server'); } if (isset($this->config['key'])) { $keyFile = file_get_contents($this->config['key']); if (class_exists('Crypt_RSA')) { $password = new \Crypt_RSA(); } elseif (class_exists('phpseclib\Crypt\RSA')) { $password = new \phpseclib\Crypt\RSA(); } else { throw new ModuleException('phpseclib/phpseclib library is not installed'); } $password->loadKey($keyFile); } if (!$this->ftp->login($user, $password)) { $this->fail('failed to authenticate user'); } } /** * @param $user * @param $password */ protected function ftpConnect($user, $password) { $this->ftp = ftp_connect($this->config['host'], $this->config['port'], $this->config['timeout']); if ($this->ftp === false) { $this->ftp = null; $this->fail('failed to connect to ftp server'); } // Login using given access details if (!@ftp_login($this->ftp, $user, $password)) { $this->fail('failed to authenticate user'); } // Set passive mode option (ftp only option) if (isset($this->config['passive'])) { ftp_pasv($this->ftp, $this->config['passive']); } } protected function isSFTP() { return strtolower($this->config['type']) == 'sftp'; } } <?php namespace Codeception\Module; use Codeception\Exception\ModuleConfigException; use Codeception\Exception\ModuleException; use Codeception\Lib\Connector\Laravel5 as LaravelConnector; use Codeception\Lib\Framework; use Codeception\Lib\Interfaces\ActiveRecord; use Codeception\Lib\Interfaces\PartedModule; use Codeception\Lib\Shared\LaravelCommon; use Codeception\Lib\ModuleContainer; use Codeception\Subscriber\ErrorHandler; use Codeception\Util\ReflectionHelper; use Illuminate\Contracts\Auth\Authenticatable; use Illuminate\Database\Eloquent\Model as EloquentModel; use Illuminate\Support\Collection; use Symfony\Component\Console\Output\OutputInterface; /** * * This module allows you to run functional tests for Laravel 5.1+ * It should **not** be used for acceptance tests. * See the Acceptance tests section below for more details. * * ## Demo project * <https://github.com/codeception/codeception-laravel5-sample> * * ## Config * * * cleanup: `boolean`, default `true` - all database queries will be run in a transaction, * which will be rolled back at the end of each test. * * run_database_migrations: `boolean`, default `false` - run database migrations before each test. * * database_migrations_path: `string`, default `null` - path to the database migrations, relative to the root of the application. * * run_database_seeder: `boolean`, default `false` - run database seeder before each test. * * database_seeder_class: `string`, default `` - database seeder class name. * * environment_file: `string`, default `.env` - the environment file to load for the tests. * * bootstrap: `string`, default `bootstrap/app.php` - relative path to app.php config file. * * root: `string`, default `` - root path of the application. * * packages: `string`, default `workbench` - root path of application packages (if any). * * vendor_dir: `string`, default `vendor` - optional relative path to vendor directory. * * disable_exception_handling: `boolean`, default `true` - disable Laravel exception handling. * * disable_middleware: `boolean`, default `false` - disable all middleware. * * disable_events: `boolean`, default `false` - disable events (does not disable model events). * * disable_model_events: `boolean`, default `false` - disable model events. * * url: `string`, default `` - the application URL. * * ### Example #1 (`functional.suite.yml`) * * Enabling module: * * ```yml * modules: * enabled: * - Laravel5 * ``` * * ### Example #2 (`functional.suite.yml`) * * Enabling module with custom .env file * * ```yml * modules: * enabled: * - Laravel5: * environment_file: .env.testing * ``` * * ## API * * * app - `Illuminate\Foundation\Application` * * config - `array` * * ## Parts * * * ORM - only include the database methods of this module: * * have * * haveMultiple * * haveRecord * * grabRecord * * seeRecord * * dontSeeRecord * * ## Acceptance tests * * You should not use this module for acceptance tests. * If you want to use Eloquent within your acceptance tests (paired with WebDriver) enable only * ORM part of this module: * * ### Example (`acceptance.suite.yml`) * * ```yaml * modules: * enabled: * - WebDriver: * browser: chrome * url: http://127.0.0.1:8000 * - Laravel5: * part: ORM * environment_file: .env.testing * ``` */ class Laravel5 extends Framework implements ActiveRecord, PartedModule { use LaravelCommon; /** * @var \Illuminate\Foundation\Application */ public $app; /** * @var array */ public $config = []; /** * Constructor. * * @param ModuleContainer $container * @param array|null $config */ public function __construct(ModuleContainer $container, $config = null) { $this->config = array_merge( [ 'cleanup' => true, 'run_database_migrations' => false, 'database_migrations_path' => null, 'run_database_seeder' => false, 'database_seeder_class' => '', 'environment_file' => '.env', 'bootstrap' => 'bootstrap' . DIRECTORY_SEPARATOR . 'app.php', 'root' => '', 'packages' => 'workbench', 'vendor_dir' => 'vendor', 'disable_exception_handling' => true, 'disable_middleware' => false, 'disable_events' => false, 'disable_model_events' => false, ], (array)$config ); $projectDir = explode($this->config['packages'], \Codeception\Configuration::projectDir())[0]; $projectDir .= $this->config['root']; $this->config['project_dir'] = $projectDir; $this->config['bootstrap_file'] = $projectDir . $this->config['bootstrap']; parent::__construct($container); } /** * @return array */ public function _parts() { return ['orm']; } /** * Initialize hook. */ public function _initialize() { $this->checkBootstrapFileExists(); $this->registerAutoloaders(); $this->revertErrorHandler(); } /** * Before hook. * * @param \Codeception\TestInterface $test */ public function _before(\Codeception\TestInterface $test) { $this->client = new LaravelConnector($this); // Database migrations should run before database cleanup transaction starts if ($this->config['run_database_migrations']) { $this->callArtisan('migrate', ['--path' => $this->config['database_migrations_path']]); } if ($this->applicationUsesDatabase() && $this->config['cleanup']) { $this->app['db']->beginTransaction(); $this->debugSection('Database', 'Transaction started'); } if ($this->config['run_database_seeder']) { $this->callArtisan('db:seed', ['--class' => $this->config['database_seeder_class'], '--force' => true ]); } } /** * After hook. * * @param \Codeception\TestInterface $test */ public function _after(\Codeception\TestInterface $test) { if ($this->applicationUsesDatabase()) { $db = $this->app['db']; if ($db instanceof \Illuminate\Database\DatabaseManager) { if ($this->config['cleanup']) { $db->rollback(); $this->debugSection('Database', 'Transaction cancelled; all changes reverted.'); } /** * Close all DB connections in order to prevent "Too many connections" issue * * @var \Illuminate\Database\Connection $connection */ foreach ($db->getConnections() as $connection) { $connection->disconnect(); } } // Remove references to Faker in factories to prevent memory leak unset($this->app[\Faker\Generator::class]); unset($this->app[\Illuminate\Database\Eloquent\Factory::class]); } } /** * Does the application use the database? * * @return bool */ private function applicationUsesDatabase() { return ! empty($this->app['config']['database.default']); } /** * Make sure the Laravel bootstrap file exists. * * @throws ModuleConfig */ protected function checkBootstrapFileExists() { $bootstrapFile = $this->config['bootstrap_file']; if (!file_exists($bootstrapFile)) { throw new ModuleConfigException( $this, "Laravel bootstrap file not found in $bootstrapFile.\n" . "Please provide a valid path by using the 'bootstrap' config param. " ); } } /** * Register Laravel autoloaders. */ protected function registerAutoloaders() { require $this->config['project_dir'] . $this->config['vendor_dir'] . DIRECTORY_SEPARATOR . 'autoload.php'; } /** * Revert back to the Codeception error handler, * because Laravel registers it's own error handler. */ protected function revertErrorHandler() { $handler = new ErrorHandler(); set_error_handler([$handler, 'errorHandler']); } /** * Provides access the Laravel application object. * * @return \Illuminate\Foundation\Application */ public function getApplication() { return $this->app; } /** * @param $app */ public function setApplication($app) { $this->app = $app; } /** * Enable Laravel exception handling. * * ``` php * <?php * $I->enableExceptionHandling(); * ?> * ``` */ public function enableExceptionHandling() { $this->client->enableExceptionHandling(); } /** * Disable Laravel exception handling. * * ``` php * <?php * $I->disableExceptionHandling(); * ?> * ``` */ public function disableExceptionHandling() { $this->client->disableExceptionHandling(); } /** * Disable middleware for the next requests. * * ``` php * <?php * $I->disableMiddleware(); * ?> * ``` */ public function disableMiddleware() { $this->client->disableMiddleware(); } /** * Disable events for the next requests. * This method does not disable model events. * To disable model events you have to use the disableModelEvents() method. * * ``` php * <?php * $I->disableEvents(); * ?> * ``` */ public function disableEvents() { $this->client->disableEvents(); } /** * Disable model events for the next requests. * * ``` php * <?php * $I->disableModelEvents(); * ?> * ``` */ public function disableModelEvents() { $this->client->disableModelEvents(); } /** * Make sure events fired during the test. * * ``` php * <?php * $I->seeEventTriggered('App\MyEvent'); * $I->seeEventTriggered(new App\Events\MyEvent()); * $I->seeEventTriggered('App\MyEvent', 'App\MyOtherEvent'); * $I->seeEventTriggered(['App\MyEvent', 'App\MyOtherEvent']); * ?> * ``` * @param $events */ public function seeEventTriggered($events) { $events = is_array($events) ? $events : func_get_args(); foreach ($events as $event) { if (!$this->client->eventTriggered($event)) { if (is_object($event)) { $event = get_class($event); } $this->fail("The '$event' event did not trigger"); } } } /** * Make sure events did not fire during the test. * * ``` php * <?php * $I->dontSeeEventTriggered('App\MyEvent'); * $I->dontSeeEventTriggered(new App\Events\MyEvent()); * $I->dontSeeEventTriggered('App\MyEvent', 'App\MyOtherEvent'); * $I->dontSeeEventTriggered(['App\MyEvent', 'App\MyOtherEvent']); * ?> * ``` * @param $events */ public function dontSeeEventTriggered($events) { $events = is_array($events) ? $events : func_get_args(); foreach ($events as $event) { if ($this->client->eventTriggered($event)) { if (is_object($event)) { $event = get_class($event); } $this->fail("The '$event' event triggered"); } } } /** * Call an Artisan command. * * ``` php * <?php * $I->callArtisan('command:name'); * $I->callArtisan('command:name', ['parameter' => 'value']); * ``` * Use 3rd parameter to pass in custom `OutputInterface` * * @param string $command * @param array $parameters * @param OutputInterface $output * @return string */ public function callArtisan($command, $parameters = [], OutputInterface $output = null) { $console = $this->app->make('Illuminate\Contracts\Console\Kernel'); if (!$output) { $console->call($command, $parameters); $output = trim($console->output()); $this->debug($output); return $output; } $console->call($command, $parameters, $output); } /** * Opens web page using route name and parameters. * * ``` php * <?php * $I->amOnRoute('posts.create'); * ?> * ``` * * @param $routeName * @param array $params */ public function amOnRoute($routeName, $params = []) { $route = $this->getRouteByName($routeName); $absolute = !is_null($route->domain()); $url = $this->app['url']->route($routeName, $params, $absolute); $this->amOnPage($url); } /** * Checks that current url matches route * * ``` php * <?php * $I->seeCurrentRouteIs('posts.index'); * ?> * ``` * @param $routeName */ public function seeCurrentRouteIs($routeName) { $this->getRouteByName($routeName); // Fails if route does not exists $currentRoute = $this->app->request->route(); $currentRouteName = $currentRoute ? $currentRoute->getName() : ''; if ($currentRouteName != $routeName) { $message = empty($currentRouteName) ? "Current route has no name" : "Current route is \"$currentRouteName\""; $this->fail($message); } } /** * Opens web page by action name * * ``` php * <?php * $I->amOnAction('PostsController@index'); * ?> * ``` * * @param $action * @param array $params */ public function amOnAction($action, $params = []) { $route = $this->getRouteByAction($action); $absolute = !is_null($route->domain()); $url = $this->app['url']->action($action, $params, $absolute); $this->amOnPage($url); } /** * Checks that current url matches action * * ``` php * <?php * $I->seeCurrentActionIs('PostsController@index'); * ?> * ``` * * @param $action */ public function seeCurrentActionIs($action) { $this->getRouteByAction($action); // Fails if route does not exists $currentRoute = $this->app->request->route(); $currentAction = $currentRoute ? $currentRoute->getActionName() : ''; $currentAction = ltrim(str_replace($this->getRootControllerNamespace(), "", $currentAction), '\\'); if ($currentAction != $action) { $this->fail("Current action is \"$currentAction\""); } } /** * @param $routeName * @return mixed */ protected function getRouteByName($routeName) { if (!$route = $this->app['routes']->getByName($routeName)) { $this->fail("Route with name '$routeName' does not exist"); } return $route; } /** * @param string $action * @return \Illuminate\Routing\Route */ protected function getRouteByAction($action) { $namespacedAction = $this->actionWithNamespace($action); if (!$route = $this->app['routes']->getByAction($namespacedAction)) { $this->fail("Action '$action' does not exist"); } return $route; } /** * Normalize an action to full namespaced action. * * @param string $action * @return string */ protected function actionWithNamespace($action) { $rootNamespace = $this->getRootControllerNamespace(); if ($rootNamespace && !(strpos($action, '\\') === 0)) { return $rootNamespace . '\\' . $action; } return trim($action, '\\'); } /** * Get the root controller namespace for the application. * * @return string */ protected function getRootControllerNamespace() { $urlGenerator = $this->app['url']; $reflection = new \ReflectionClass($urlGenerator); $property = $reflection->getProperty('rootNamespace'); $property->setAccessible(true); return $property->getValue($urlGenerator); } /** * Assert that a session variable exists. * * ``` php * <?php * $I->seeInSession('key'); * $I->seeInSession('key', 'value'); * ?> * ``` * * @param string|array $key * @param mixed|null $value * @return void */ public function seeInSession($key, $value = null) { if (is_array($key)) { $this->seeSessionHasValues($key); return; } if (! $this->app['session']->has($key)) { $this->fail("No session variable with key '$key'"); } if (! is_null($value)) { $this->assertEquals($value, $this->app['session']->get($key)); } } /** * Assert that the session has a given list of values. * * ``` php * <?php * $I->seeSessionHasValues(['key1', 'key2']); * $I->seeSessionHasValues(['key1' => 'value1', 'key2' => 'value2']); * ?> * ``` * * @param array $bindings * @return void */ public function seeSessionHasValues(array $bindings) { foreach ($bindings as $key => $value) { if (is_int($key)) { $this->seeInSession($value); } else { $this->seeInSession($key, $value); } } } /** * Assert that form errors are bound to the View. * * ``` php * <?php * $I->seeFormHasErrors(); * ?> * ``` * * @return void */ public function seeFormHasErrors() { $viewErrorBag = $this->app->make('view')->shared('errors'); $this->assertGreaterThan(0, count($viewErrorBag), 'Expecting that the form has errors, but there were none!'); } /** * Assert that there are no form errors bound to the View. * * ``` php * <?php * $I->dontSeeFormErrors(); * ?> * ``` * * @return void */ public function dontSeeFormErrors() { $viewErrorBag = $this->app->make('view')->shared('errors'); $this->assertEquals(0, count($viewErrorBag), 'Expecting that the form does not have errors, but there were!'); } /** * Assert that specific form error messages are set in the view. * * This method calls `seeFormErrorMessage` for each entry in the `$bindings` array. * * ``` php * <?php * $I->seeFormErrorMessages([ * 'username' => 'Invalid Username', * 'password' => null, * ]); * ?> * ``` * @param array $bindings */ public function seeFormErrorMessages(array $bindings) { foreach ($bindings as $key => $value) { $this->seeFormErrorMessage($key, $value); } } /** * Assert that a specific form error message is set in the view. * * If you want to assert that there is a form error message for a specific key * but don't care about the actual error message you can omit `$expectedErrorMessage`. * * If you do pass `$expectedErrorMessage`, this method checks if the actual error message for a key * contains `$expectedErrorMessage`. * * ``` php * <?php * $I->seeFormErrorMessage('username'); * $I->seeFormErrorMessage('username', 'Invalid Username'); * ?> * ``` * @param string $key * @param string|null $expectedErrorMessage */ public function seeFormErrorMessage($key, $expectedErrorMessage = null) { $viewErrorBag = $this->app['view']->shared('errors'); if (!($viewErrorBag->has($key))) { $this->fail("No form error message for key '$key'\n"); } if (! is_null($expectedErrorMessage)) { $this->assertStringContainsString($expectedErrorMessage, $viewErrorBag->first($key)); } } /** * Set the currently logged in user for the application. * Takes either an object that implements the User interface or * an array of credentials. * * ``` php * <?php * // provide array of credentials * $I->amLoggedAs(['username' => 'jane@example.com', 'password' => 'password']); * * // provide User object * $I->amLoggedAs( new User ); * * // can be verified with $I->seeAuthentication(); * ?> * ``` * @param \Illuminate\Contracts\Auth\User|array $user * @param string|null $driver The authentication driver for Laravel <= 5.1.*, guard name for Laravel >= 5.2 * @return void */ public function amLoggedAs($user, $driver = null) { $guard = $auth = $this->app['auth']; if (method_exists($auth, 'driver')) { $guard = $auth->driver($driver); } if (method_exists($auth, 'guard')) { $guard = $auth->guard($driver); } if ($user instanceof Authenticatable) { $guard->login($user); return; } $this->assertTrue($guard->attempt($user), 'Failed to login with credentials ' . json_encode($user)); } /** * Logout user. */ public function logout() { $this->app['auth']->logout(); } /** * Checks that a user is authenticated. * You can specify the guard that should be use for Laravel >= 5.2. * @param string|null $guard */ public function seeAuthentication($guard = null) { $auth = $this->app['auth']; if (method_exists($auth, 'guard')) { $auth = $auth->guard($guard); } $this->assertTrue($auth->check(), 'There is no authenticated user'); } /** * Check that user is not authenticated. * You can specify the guard that should be use for Laravel >= 5.2. * @param string|null $guard */ public function dontSeeAuthentication($guard = null) { $auth = $this->app['auth']; if (method_exists($auth, 'guard')) { $auth = $auth->guard($guard); } $this->assertNotTrue($auth->check(), 'There is an user authenticated'); } /** * Return an instance of a class from the Laravel service container. * (https://laravel.com/docs/master/container) * * ``` php * <?php * // In Laravel * App::bind('foo', function($app) * { * return new FooBar; * }); * * // Then in test * $service = $I->grabService('foo'); * * // Will return an instance of FooBar, also works for singletons. * ?> * ``` * * @param string $class * @return mixed */ public function grabService($class) { return $this->app[$class]; } /** * Inserts record into the database. * If you pass the name of a database table as the first argument, this method returns an integer ID. * You can also pass the class name of an Eloquent model, in that case this method returns an Eloquent model. * * ``` php * <?php * $user_id = $I->haveRecord('users', array('name' => 'Davert')); // returns integer * $user = $I->haveRecord('App\User', array('name' => 'Davert')); // returns Eloquent model * ?> * ``` * * @param string $table * @param array $attributes * @return EloquentModel|int * @throws \RuntimeException * @part orm */ public function haveRecord($table, $attributes = []) { if (class_exists($table)) { $model = new $table; if (! $model instanceof EloquentModel) { throw new \RuntimeException("Class $table is not an Eloquent model"); } $model->fill($attributes)->save(); return $model; } try { return $this->app['db']->table($table)->insertGetId($attributes); } catch (\Exception $e) { $this->fail("Could not insert record into table '$table':\n\n" . $e->getMessage()); } } /** * Checks that record exists in database. * You can pass the name of a database table or the class name of an Eloquent model as the first argument. * * ``` php * <?php * $I->seeRecord('users', array('name' => 'davert')); * $I->seeRecord('App\User', array('name' => 'davert')); * ?> * ``` * * @param string $table * @param array $attributes * @part orm */ public function seeRecord($table, $attributes = []) { if (class_exists($table)) { if (! $this->findModel($table, $attributes)) { $this->fail("Could not find $table with " . json_encode($attributes)); } } elseif (! $this->findRecord($table, $attributes)) { $this->fail("Could not find matching record in table '$table'"); } $this->assertTrue(true); } /** * Checks that record does not exist in database. * You can pass the name of a database table or the class name of an Eloquent model as the first argument. * * ``` php * <?php * $I->dontSeeRecord('users', array('name' => 'davert')); * $I->dontSeeRecord('App\User', array('name' => 'davert')); * ?> * ``` * * @param string $table * @param array $attributes * @part orm */ public function dontSeeRecord($table, $attributes = []) { if (class_exists($table)) { if ($this->findModel($table, $attributes)) { $this->fail("Unexpectedly found matching $table with " . json_encode($attributes)); } } elseif ($this->findRecord($table, $attributes)) { $this->fail("Unexpectedly found matching record in table '$table'"); } $this->assertTrue(true); } /** * Retrieves record from database * If you pass the name of a database table as the first argument, this method returns an array. * You can also pass the class name of an Eloquent model, in that case this method returns an Eloquent model. * * ``` php * <?php * $record = $I->grabRecord('users', array('name' => 'davert')); // returns array * $record = $I->grabRecord('App\User', array('name' => 'davert')); // returns Eloquent model * ?> * ``` * * @param string $table * @param array $attributes * @return array|EloquentModel * @part orm */ public function grabRecord($table, $attributes = []) { if (class_exists($table)) { if (! $model = $this->findModel($table, $attributes)) { $this->fail("Could not find $table with " . json_encode($attributes)); } return $model; } if (! $record = $this->findRecord($table, $attributes)) { $this->fail("Could not find matching record in table '$table'"); } return $record; } /** * Checks that number of given records were found in database. * You can pass the name of a database table or the class name of an Eloquent model as the first argument. * * ``` php * <?php * $I->seeNumRecords(1, 'users', array('name' => 'davert')); * $I->seeNumRecords(1, 'App\User', array('name' => 'davert')); * ?> * ``` * * @param integer $expectedNum * @param string $table * @param array $attributes * @part orm */ public function seeNumRecords($expectedNum, $table, $attributes = []) { if (class_exists($table)) { $currentNum = $this->countModels($table, $attributes); $this->assertEquals( $expectedNum, $currentNum, "The number of found {$table} ({$currentNum}) does not match expected number {$expectedNum} with " . json_encode($attributes) ); } else { $currentNum = $this->countRecords($table, $attributes); $this->assertEquals( $expectedNum, $currentNum, "The number of found records in table {$table} ({$currentNum}) does not match expected number $expectedNum with " . json_encode($attributes) ); } } /** * Retrieves number of records from database * You can pass the name of a database table or the class name of an Eloquent model as the first argument. * * ``` php * <?php * $I->grabNumRecords('users', array('name' => 'davert')); * $I->grabNumRecords('App\User', array('name' => 'davert')); * ?> * ``` * * @param string $table * @param array $attributes * @return integer * @part orm */ public function grabNumRecords($table, $attributes = []) { return class_exists($table)? $this->countModels($table, $attributes) : $this->countRecords($table, $attributes); } /** * @param string $modelClass * @param array $attributes * * @return EloquentModel */ protected function findModel($modelClass, $attributes = []) { $query = $this->buildQuery($modelClass, $attributes); return $query->first(); } /** * @param string $table * @param array $attributes * @return array */ protected function findRecord($table, $attributes = []) { $query = $this->buildQuery($table, $attributes); return (array) $query->first(); } /** * @param string $modelClass * @param array $attributes * @return integer */ protected function countModels($modelClass, $attributes = []) { $query = $this->buildQuery($modelClass, $attributes); return $query->count(); } /** * @param string $table * @param array $attributes * @return integer */ protected function countRecords($table, $attributes = []) { $query = $this->buildQuery($table, $attributes); return $query->count(); } /** * @param string $modelClass * * @return EloquentModel * @throws \RuntimeException */ protected function getQueryBuilderFromModel($modelClass) { $model = new $modelClass; if (!$model instanceof EloquentModel) { throw new \RuntimeException("Class $modelClass is not an Eloquent model"); } return $model->newQuery(); } /** * @param string $table * * @return EloquentModel */ protected function getQueryBuilderFromTable($table) { return $this->app['db']->table($table); } /** * Use Laravel's model factory to create a model. * Can only be used with Laravel 5.1 and later. * * ``` php * <?php * $I->have('App\User'); * $I->have('App\User', ['name' => 'John Doe']); * $I->have('App\User', [], 'admin'); * ?> * ``` * * @see http://laravel.com/docs/5.1/testing#model-factories * @param string $model * @param array $attributes * @param string $name * @return mixed * @part orm */ public function have($model, $attributes = [], $name = 'default') { try { $result = $this->modelFactory($model, $name)->create($attributes); // Since Laravel 5.4 the model factory returns a collection instead of a single object if ($result instanceof Collection) { $result = $result[0]; } return $result; } catch (\Exception $e) { $this->fail("Could not create model: \n\n" . get_class($e) . "\n\n" . $e->getMessage()); } } /** * Use Laravel's model factory to create multiple models. * Can only be used with Laravel 5.1 and later. * * ``` php * <?php * $I->haveMultiple('App\User', 10); * $I->haveMultiple('App\User', 10, ['name' => 'John Doe']); * $I->haveMultiple('App\User', 10, [], 'admin'); * ?> * ``` * * @see http://laravel.com/docs/5.1/testing#model-factories * @param string $model * @param int $times * @param array $attributes * @param string $name * @return mixed * @part orm */ public function haveMultiple($model, $times, $attributes = [], $name = 'default') { try { return $this->modelFactory($model, $name, $times)->create($attributes); } catch (\Exception $e) { $this->fail("Could not create model: \n\n" . get_class($e) . "\n\n" . $e->getMessage()); } } /** * @param string $model * @param string $name * @param int $times * @return \Illuminate\Database\Eloquent\FactoryBuilder * @throws ModuleException */ protected function modelFactory($model, $name, $times = 1) { if (! function_exists('factory')) { throw new ModuleException($this, 'The factory() method does not exist. ' . 'This functionality relies on Laravel model factories, which were introduced in Laravel 5.1.'); } return factory($model, $name, $times); } /** * Returns a list of recognized domain names. * This elements of this list are regular expressions. * * @return array */ protected function getInternalDomains() { $internalDomains = [$this->getApplicationDomainRegex()]; foreach ($this->app['routes'] as $route) { if (!is_null($route->domain())) { $internalDomains[] = $this->getDomainRegex($route); } } return array_unique($internalDomains); } /** * @return string */ private function getApplicationDomainRegex() { $server = ReflectionHelper::readPrivateProperty($this->client, 'server'); $domain = $server['HTTP_HOST']; return '/^' . str_replace('.', '\.', $domain) . '$/'; } /** * Get the regex for matching the domain part of this route. * * @param \Illuminate\Routing\Route $route * @return string */ private function getDomainRegex($route) { ReflectionHelper::invokePrivateMethod($route, 'compileRoute'); $compiledRoute = ReflectionHelper::readPrivateProperty($route, 'compiled'); return $compiledRoute->getHostRegex(); } /** * Build Eloquent query with attributes * * @param string $table * @param array $attributes * @return EloquentModel * @part orm */ private function buildQuery($table, $attributes = []) { if (class_exists($table)) { $query = $this->getQueryBuilderFromModel($table); } else { $query = $this->getQueryBuilderFromTable($table); } foreach ($attributes as $key => $value) { if (\is_array($value)) { call_user_func_array(array($query, "where"), $value); } elseif (is_null($value)) { $query->whereNull($key); } else { $query->where($key, $value); } } return $query; } } <?php namespace Codeception\Module; use Codeception\Configuration; use Codeception\Exception\ModuleException; use Codeception\Exception\ModuleConfigException; use Codeception\Lib\Connector\Lumen as LumenConnector; use Codeception\Lib\Framework; use Codeception\Lib\Interfaces\ActiveRecord; use Codeception\Lib\Interfaces\PartedModule; use Codeception\Lib\Shared\LaravelCommon; use Codeception\Lib\ModuleContainer; use Codeception\TestInterface; use Codeception\Util\ReflectionHelper; use Illuminate\Contracts\Auth\Authenticatable; use Illuminate\Database\Eloquent\Model as EloquentModel; /** * * This module allows you to run functional tests for Lumen. * Please try it and leave your feedback. * * ## Demo project * <https://github.com/janhenkgerritsen/codeception-lumen-sample> * * ## Status * * * Maintainer: **Jan-Henk Gerritsen** * * Stability: **dev** * * Contact: janhenkgerritsen@gmail.com * * ## Config * * * cleanup: `boolean`, default `true` - all database queries will be run in a transaction, * which will be rolled back at the end of each test. * * bootstrap: `string`, default `bootstrap/app.php` - relative path to app.php config file. * * root: `string`, default `` - root path of the application. * * packages: `string`, default `workbench` - root path of application packages (if any). * * url: `string`, default `http://localhost` - the application URL * * ## API * * * app - `\Laravel\Lumen\Application` * * config - `array` * * ## Parts * * * ORM - only include the database methods of this module: * * have * * haveMultiple * * haveRecord * * grabRecord * * seeRecord * * dontSeeRecord */ class Lumen extends Framework implements ActiveRecord, PartedModule { use LaravelCommon; /** * @var \Laravel\Lumen\Application */ public $app; /** * @var array */ public $config = []; /** * Constructor. * * @param ModuleContainer $container * @param array|null $config */ public function __construct(ModuleContainer $container, $config = null) { $this->config = array_merge( [ 'cleanup' => true, 'bootstrap' => 'bootstrap' . DIRECTORY_SEPARATOR . 'app.php', 'root' => '', 'packages' => 'workbench', 'url' => 'http://localhost', ], (array)$config ); $projectDir = explode($this->config['packages'], Configuration::projectDir())[0]; $projectDir .= $this->config['root']; $this->config['project_dir'] = $projectDir; $this->config['bootstrap_file'] = $projectDir . $this->config['bootstrap']; parent::__construct($container); } /** * @return array */ public function _parts() { return ['orm']; } /** * Initialize hook. */ public function _initialize() { $this->checkBootstrapFileExists(); $this->registerAutoloaders(); } /** * Before hook. * * @param \Codeception\TestInterface $test * @throws ModuleConfigException */ public function _before(TestInterface $test) { $this->client = new LumenConnector($this); if ($this->app['db'] && $this->config['cleanup']) { $this->app['db']->beginTransaction(); } } /** * After hook. * * @param \Codeception\TestInterface $test */ public function _after(TestInterface $test) { if ($this->app['db'] && $this->config['cleanup']) { $this->app['db']->rollback(); } // disconnect from DB to prevent "Too many connections" issue if ($this->app['db']) { $this->app['db']->disconnect(); } } /** * Make sure the Lumen bootstrap file exists. * * @throws ModuleConfigException */ protected function checkBootstrapFileExists() { $bootstrapFile = $this->config['bootstrap_file']; if (!file_exists($bootstrapFile)) { throw new ModuleConfigException( $this, "Lumen bootstrap file not found in $bootstrapFile.\n" . "Please provide a valid path using the 'bootstrap' config param. " ); } } /** * Register autoloaders. */ protected function registerAutoloaders() { require $this->config['project_dir'] . 'vendor' . DIRECTORY_SEPARATOR . 'autoload.php'; } /** * Provides access the Lumen application object. * * @return \Laravel\Lumen\Application */ public function getApplication() { return $this->app; } /** * @param \Laravel\Lumen\Application $app */ public function setApplication($app) { $this->app = $app; } /** * Opens web page using route name and parameters. * * ```php * <?php * $I->amOnRoute('homepage'); * ?> * ``` * * @param $routeName * @param array $params */ public function amOnRoute($routeName, $params = []) { $route = $this->getRouteByName($routeName); if (!$route) { $this->fail("Could not find route with name '$routeName'"); } $url = $this->generateUrlForRoute($route, $params); $this->amOnPage($url); } /** * Get the route for a route name. * * @param string $routeName * @return array|null */ private function getRouteByName($routeName) { if (isset($this->app->router) && $this->app->router instanceof \Laravel\Lumen\Routing\Router) { $router = $this->app->router; } else { // backward compatibility with lumen 5.3 $router = $this->app; } foreach ($router->getRoutes() as $route) { if (isset($route['action']['as']) && $route['action']['as'] == $routeName) { return $route; } } $this->fail("Route with name '$routeName' does not exist"); return null; } /** * Generate the URL for a route specification. * Replaces the route parameters from left to right with the parameters * passed in the $params array. * * @param array $route * @param array $params * @return string */ private function generateUrlForRoute($route, $params) { $url = $route['uri']; while (count($params) > 0) { $param = array_shift($params); $url = preg_replace('/{.+?}/', $param, $url, 1); } return $url; } /** * Set the authenticated user for the next request. * This will not persist between multiple requests. * * @param \Illuminate\Contracts\Auth\Authenticatable * @param string|null $driver The authentication driver for Lumen <= 5.1.*, guard name for Lumen >= 5.2 * @return void */ public function amLoggedAs($user, $driver = null) { if (!$user instanceof Authenticatable) { $this->fail( 'The user passed to amLoggedAs() should be an instance of \\Illuminate\\Contracts\\Auth\\Authenticatable' ); } $guard = $auth = $this->app['auth']; if (method_exists($auth, 'driver')) { $guard = $auth->driver($driver); } if (method_exists($auth, 'guard')) { $guard = $auth->guard($driver); } $guard->setUser($user); } /** * Checks that user is authenticated. */ public function seeAuthentication() { $this->assertTrue($this->app['auth']->check(), 'User is not logged in'); } /** * Check that user is not authenticated. */ public function dontSeeAuthentication() { $this->assertFalse($this->app['auth']->check(), 'User is logged in'); } /** * Return an instance of a class from the IoC Container. * * Example * ``` php * <?php * // In Lumen * App::bind('foo', function($app) * { * return new FooBar; * }); * * // Then in test * $service = $I->grabService('foo'); * * // Will return an instance of FooBar, also works for singletons. * ?> * ``` * * @param string $class * @return mixed */ public function grabService($class) { return $this->app[$class]; } /** * Inserts record into the database. * If you pass the name of a database table as the first argument, this method returns an integer ID. * You can also pass the class name of an Eloquent model, in that case this method returns an Eloquent model. * * ``` php * <?php * $user_id = $I->haveRecord('users', array('name' => 'Davert')); // returns integer * $user = $I->haveRecord('App\User', array('name' => 'Davert')); // returns Eloquent model * ?> * ``` * * @param string $table * @param array $attributes * @return integer|EloquentModel * @part orm */ public function haveRecord($table, $attributes = []) { if (class_exists($table)) { $model = new $table; if (!$model instanceof EloquentModel) { throw new \RuntimeException("Class $table is not an Eloquent model"); } $model->fill($attributes)->save(); return $model; } try { return $this->app['db']->table($table)->insertGetId($attributes); } catch (\Exception $e) { $this->fail("Could not insert record into table '$table':\n\n" . $e->getMessage()); } } /** * Checks that record exists in database. * You can pass the name of a database table or the class name of an Eloquent model as the first argument. * * ``` php * <?php * $I->seeRecord('users', array('name' => 'davert')); * $I->seeRecord('App\User', array('name' => 'davert')); * ?> * ``` * * @param string $table * @param array $attributes * @part orm */ public function seeRecord($table, $attributes = []) { if (class_exists($table)) { if (!$this->findModel($table, $attributes)) { $this->fail("Could not find $table with " . json_encode($attributes)); } } elseif (!$this->findRecord($table, $attributes)) { $this->fail("Could not find matching record in table '$table'"); } } /** * Checks that record does not exist in database. * You can pass the name of a database table or the class name of an Eloquent model as the first argument. * * ``` php * <?php * $I->dontSeeRecord('users', array('name' => 'davert')); * $I->dontSeeRecord('App\User', array('name' => 'davert')); * ?> * ``` * * @param string $table * @param array $attributes * @part orm */ public function dontSeeRecord($table, $attributes = []) { if (class_exists($table)) { if ($this->findModel($table, $attributes)) { $this->fail("Unexpectedly found matching $table with " . json_encode($attributes)); } } elseif ($this->findRecord($table, $attributes)) { $this->fail("Unexpectedly found matching record in table '$table'"); } } /** * Retrieves record from database * If you pass the name of a database table as the first argument, this method returns an array. * You can also pass the class name of an Eloquent model, in that case this method returns an Eloquent model. * * ``` php * <?php * $record = $I->grabRecord('users', array('name' => 'davert')); // returns array * $record = $I->grabRecord('App\User', array('name' => 'davert')); // returns Eloquent model * ?> * ``` * * @param string $table * @param array $attributes * @return array|EloquentModel * @part orm */ public function grabRecord($table, $attributes = []) { if (class_exists($table)) { if (!$model = $this->findModel($table, $attributes)) { $this->fail("Could not find $table with " . json_encode($attributes)); } return $model; } if (!$record = $this->findRecord($table, $attributes)) { $this->fail("Could not find matching record in table '$table'"); } return $record; } /** * @param string $modelClass * @param array $attributes * * @return EloquentModel */ protected function findModel($modelClass, $attributes = []) { $model = new $modelClass; if (!$model instanceof EloquentModel) { throw new \RuntimeException("Class $modelClass is not an Eloquent model"); } $query = $model->newQuery(); foreach ($attributes as $key => $value) { $query->where($key, $value); } return $query->first(); } /** * @param string $table * @param array $attributes * @return array */ protected function findRecord($table, $attributes = []) { $query = $this->app['db']->table($table); foreach ($attributes as $key => $value) { $query->where($key, $value); } return (array)$query->first(); } /** * Use Lumen's model factory to create a model. * Can only be used with Lumen 5.1 and later. * * ``` php * <?php * $I->have('App\User'); * $I->have('App\User', ['name' => 'John Doe']); * $I->have('App\User', [], 'admin'); * ?> * ``` * * @see https://lumen.laravel.com/docs/master/testing#model-factories * @param string $model * @param array $attributes * @param string $name * @return mixed * @part orm */ public function have($model, $attributes = [], $name = 'default') { try { return $this->modelFactory($model, $name)->create($attributes); } catch (\Exception $e) { $this->fail("Could not create model: \n\n" . get_class($e) . "\n\n" . $e->getMessage()); } } /** * Use Laravel's model factory to create multiple models. * Can only be used with Lumen 5.1 and later. * * ``` php * <?php * $I->haveMultiple('App\User', 10); * $I->haveMultiple('App\User', 10, ['name' => 'John Doe']); * $I->haveMultiple('App\User', 10, [], 'admin'); * ?> * ``` * * @see https://lumen.laravel.com/docs/master/testing#model-factories * @param string $model * @param int $times * @param array $attributes * @param string $name * @return mixed * @part orm */ public function haveMultiple($model, $times, $attributes = [], $name = 'default') { try { return $this->modelFactory($model, $name, $times)->create($attributes); } catch (\Exception $e) { $this->fail("Could not create model: \n\n" . get_class($e) . "\n\n" . $e->getMessage()); } } /** * @param string $model * @param string $name * @param int $times * @return \Illuminate\Database\Eloquent\FactoryBuilder * @throws ModuleException */ protected function modelFactory($model, $name, $times = 1) { if (!function_exists('factory')) { throw new ModuleException($this, 'The factory() method does not exist. ' . 'This functionality relies on Lumen model factories, which were introduced in Lumen 5.1.'); } return factory($model, $name, $times); } /** * Returns a list of recognized domain names. * This elements of this list are regular expressions. * * @return array */ protected function getInternalDomains() { $server = ReflectionHelper::readPrivateProperty($this->client, 'server'); return ['/^' . str_replace('.', '\.', $server['HTTP_HOST']) . '$/']; } } <?php namespace Codeception\Module; use Codeception\Module as CodeceptionModule; use Codeception\TestInterface; use Codeception\Exception\ModuleConfigException; /** * Connects to [memcached](http://www.memcached.org/) using either _Memcache_ or _Memcached_ extension. * * Performs a cleanup by flushing all values after each test run. * * ## Status * * * Maintainer: **davert** * * Stability: **beta** * * Contact: davert@codeception.com * * ## Configuration * * * **`host`** (`string`, default `'localhost'`) - The memcached host * * **`port`** (`int`, default `11211`) - The memcached port * * ### Example (`unit.suite.yml`) * * ```yaml * modules: * - Memcache: * host: 'localhost' * port: 11211 * ``` * * Be sure you don't use the production server to connect. * * ## Public Properties * * * **memcache** - instance of _Memcache_ or _Memcached_ object * */ class Memcache extends CodeceptionModule { /** * @var \Memcache|\Memcached */ public $memcache = null; /** * {@inheritdoc} */ protected $config = [ 'host' => 'localhost', 'port' => 11211 ]; /** * Code to run before each test. * * @param TestInterface $test * @throws ModuleConfigException */ public function _before(TestInterface $test) { if (class_exists('\Memcache')) { $this->memcache = new \Memcache; $this->memcache->connect($this->config['host'], $this->config['port']); } elseif (class_exists('\Memcached')) { $this->memcache = new \Memcached; $this->memcache->addServer($this->config['host'], $this->config['port']); } else { throw new ModuleConfigException(__CLASS__, 'Memcache classes not loaded'); } } /** * Code to run after each test. * * @param TestInterface $test */ public function _after(TestInterface $test) { if (empty($this->memcache)) { return; } $this->memcache->flush(); switch (get_class($this->memcache)) { case 'Memcache': $this->memcache->close(); break; case 'Memcached': $this->memcache->quit(); break; } } /** * Grabs value from memcached by key. * * Example: * * ``` php * <?php * $users_count = $I->grabValueFromMemcached('users_count'); * ?> * ``` * * @param $key * @return array|string */ public function grabValueFromMemcached($key) { $value = $this->memcache->get($key); $this->debugSection("Value", $value); return $value; } /** * Checks item in Memcached exists and the same as expected. * * Examples: * * ``` php * <?php * // With only one argument, only checks the key exists * $I->seeInMemcached('users_count'); * * // Checks a 'users_count' exists and has the value 200 * $I->seeInMemcached('users_count', 200); * ?> * ``` * * @param $key * @param $value */ public function seeInMemcached($key, $value = null) { $actual = $this->memcache->get($key); $this->debugSection("Value", $actual); if (null === $value) { $this->assertNotFalse($actual, "Cannot find key '$key' in Memcached"); } else { $this->assertEquals($value, $actual, "Cannot find key '$key' in Memcached with the provided value"); } } /** * Checks item in Memcached doesn't exist or is the same as expected. * * Examples: * * ``` php * <?php * // With only one argument, only checks the key does not exist * $I->dontSeeInMemcached('users_count'); * * // Checks a 'users_count' exists does not exist or its value is not the one provided * $I->dontSeeInMemcached('users_count', 200); * ?> * ``` * * @param $key * @param $value */ public function dontSeeInMemcached($key, $value = null) { $actual = $this->memcache->get($key); $this->debugSection("Value", $actual); if (null === $value) { $this->assertFalse($actual, "The key '$key' exists in Memcached"); } else { if (false !== $actual) { $this->assertEquals($value, $actual, "The key '$key' exists in Memcached with the provided value"); } } } /** * Stores an item `$value` with `$key` on the Memcached server. * * @param string $key * @param mixed $value * @param int $expiration */ public function haveInMemcached($key, $value, $expiration = null) { switch (get_class($this->memcache)) { case 'Memcache': $this->assertTrue($this->memcache->set($key, $value, null, $expiration)); break; case 'Memcached': $this->assertTrue($this->memcache->set($key, $value, $expiration)); break; } } /** * Flushes all Memcached data. */ public function clearMemcache() { $this->memcache->flush(); } } <?php namespace Codeception\Module; use Codeception\Lib\Interfaces\RequiresPackage; use Codeception\Module as CodeceptionModule; use Codeception\Configuration as Configuration; use Codeception\Exception\ModuleConfigException; use Codeception\Exception\ModuleException; use Codeception\Lib\Driver\MongoDb as MongoDbDriver; use Codeception\TestInterface; /** * Works with MongoDb database. * * The most important function of this module is cleaning database before each test. * To have your database properly cleaned you should configure it to access the database. * * In order to have your database populated with data you need a valid js file with data (of the same style which can be fed up to mongo binary) * File can be generated by RockMongo export command * You can also use directory, generated by ```mongodump``` tool or it's ```.tar.gz``` archive (not available for Windows systems), generated by ```tar -czf <archive_file_name>.tar.gz <path_to dump directory>```. * Just put it in ``` tests/_data ``` dir (by default) and specify path to it in config. * Next time after database is cleared all your data will be restored from dump. * The DB preparation should as following: * - clean database * - system collection system.users should contain the user which will be authenticated while script performs DB operations * * Connection is done by MongoDb driver, which is stored in Codeception\Lib\Driver namespace. * Check out the driver if you get problems loading dumps and cleaning databases. * * HINT: This module can be used with [Mongofill](https://github.com/mongofill/mongofill) library which is Mongo client written in PHP without extension. * * ## Status * * * Maintainer: **judgedim**, **davert** * * Stability: **beta** * * Contact: davert@codeception.com * * *Please review the code of non-stable modules and provide patches if you have issues.* * * ## Config * * * dsn *required* - MongoDb DSN with the db name specified at the end of the host after slash * * user *required* - user to access database * * password *required* - password * * dump_type *required* - type of dump. * One of 'js' (MongoDb::DUMP_TYPE_JS), 'mongodump' (MongoDb::DUMP_TYPE_MONGODUMP) or 'mongodump-tar-gz' (MongoDb::DUMP_TYPE_MONGODUMP_TAR_GZ). * default: MongoDb::DUMP_TYPE_JS). * * dump - path to database dump * * populate: true - should the dump be loaded before test suite is started. * * cleanup: true - should the dump be reloaded after each test * */ class MongoDb extends CodeceptionModule implements RequiresPackage { const DUMP_TYPE_JS = 'js'; const DUMP_TYPE_MONGODUMP = 'mongodump'; const DUMP_TYPE_MONGODUMP_TAR_GZ = 'mongodump-tar-gz'; /** * @api * @var */ public $dbh; /** * @var */ protected $dumpFile; protected $isDumpFileEmpty = true; protected $config = [ 'populate' => true, 'cleanup' => true, 'dump' => null, 'dump_type' => self::DUMP_TYPE_JS, 'user' => null, 'password' => null, 'quiet' => false, ]; protected $populated = false; /** * @var \Codeception\Lib\Driver\MongoDb */ public $driver; protected $requiredFields = ['dsn']; public function _initialize() { try { $this->driver = MongoDbDriver::create( $this->config['dsn'], $this->config['user'], $this->config['password'] ); } catch (\MongoConnectionException $e) { throw new ModuleException(__CLASS__, $e->getMessage() . ' while creating Mongo connection'); } // starting with loading dump if ($this->config['populate']) { $this->cleanup(); $this->loadDump(); $this->populated = true; } } private function validateDump() { if ($this->config['dump'] && ($this->config['cleanup'] or ($this->config['populate']))) { if (!file_exists(Configuration::projectDir() . $this->config['dump'])) { throw new ModuleConfigException( __CLASS__, "File with dump doesn't exist.\n Please, check path for dump file: " . $this->config['dump'] ); } $this->dumpFile = Configuration::projectDir() . $this->config['dump']; $this->isDumpFileEmpty = false; if ($this->config['dump_type'] === self::DUMP_TYPE_JS) { $content = file_get_contents($this->dumpFile); $content = trim(preg_replace('%/\*(?:(?!\*/).)*\*/%s', "", $content)); if (!sizeof(explode("\n", $content))) { $this->isDumpFileEmpty = true; } return; } if ($this->config['dump_type'] === self::DUMP_TYPE_MONGODUMP) { if (!is_dir($this->dumpFile)) { throw new ModuleConfigException( __CLASS__, "Dump must be a directory.\n Please, check dump: " . $this->config['dump'] ); } $this->isDumpFileEmpty = true; $dumpDir = dir($this->dumpFile); while (false !== ($entry = $dumpDir->read())) { if ($entry !== '..' && $entry !== '.') { $this->isDumpFileEmpty = false; break; } } $dumpDir->close(); return; } if ($this->config['dump_type'] === self::DUMP_TYPE_MONGODUMP_TAR_GZ) { if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') { throw new ModuleConfigException( __CLASS__, "Tar gunzip archives are not supported for Windows systems" ); } if (!preg_match('/(\.tar\.gz|\.tgz)$/', $this->dumpFile)) { throw new ModuleConfigException( __CLASS__, "Dump file must be a valid tar gunzip archive.\n Please, check dump file: " . $this->config['dump'] ); } return; } throw new ModuleConfigException( __CLASS__, '\"dump_type\" must be one of ["' . self::DUMP_TYPE_JS . '", "' . self::DUMP_TYPE_MONGODUMP . '", "' . self::DUMP_TYPE_MONGODUMP_TAR_GZ . '"].' ); } } public function _before(TestInterface $test) { if ($this->config['cleanup'] && !$this->populated) { $this->cleanup(); $this->loadDump(); } } public function _after(TestInterface $test) { $this->populated = false; } protected function cleanup() { $dbh = $this->driver->getDbh(); if (!$dbh) { throw new ModuleConfigException( __CLASS__, "No connection to database. Remove this module from config if you don't need database repopulation" ); } try { $this->driver->cleanup(); } catch (\Exception $e) { throw new ModuleException(__CLASS__, $e->getMessage()); } } protected function loadDump() { $this->validateDump(); if ($this->isDumpFileEmpty) { return; } try { if ($this->config['dump_type'] === self::DUMP_TYPE_JS) { $this->driver->load($this->dumpFile); } if ($this->config['dump_type'] === self::DUMP_TYPE_MONGODUMP) { $this->driver->setQuiet($this->config['quiet']); $this->driver->loadFromMongoDump($this->dumpFile); } if ($this->config['dump_type'] === self::DUMP_TYPE_MONGODUMP_TAR_GZ) { $this->driver->setQuiet($this->config['quiet']); $this->driver->loadFromTarGzMongoDump($this->dumpFile); } } catch (\Exception $e) { throw new ModuleException(__CLASS__, $e->getMessage()); } } /** * Specify the database to use * * ``` php * <?php * $I->useDatabase('db_1'); * ``` * * @param $dbName */ public function useDatabase($dbName) { $this->driver->setDatabase($dbName); } /** * Inserts data into collection * * ``` php * <?php * $I->haveInCollection('users', array('name' => 'John', 'email' => 'john@coltrane.com')); * $user_id = $I->haveInCollection('users', array('email' => 'john@coltrane.com')); * ``` * * @param $collection * @param array $data */ public function haveInCollection($collection, array $data) { $collection = $this->driver->getDbh()->selectCollection($collection); if ($this->driver->isLegacy()) { $collection->insert($data); return $data['_id']; } $response = $collection->insertOne($data); return (string) $response->getInsertedId(); } /** * Checks if collection contains an item. * * ``` php * <?php * $I->seeInCollection('users', array('name' => 'miles')); * ``` * * @param $collection * @param array $criteria */ public function seeInCollection($collection, $criteria = []) { $collection = $this->driver->getDbh()->selectCollection($collection); $res = $collection->count($criteria); \PHPUnit\Framework\Assert::assertGreaterThan(0, $res); } /** * Checks if collection doesn't contain an item. * * ``` php * <?php * $I->dontSeeInCollection('users', array('name' => 'miles')); * ``` * * @param $collection * @param array $criteria */ public function dontSeeInCollection($collection, $criteria = []) { $collection = $this->driver->getDbh()->selectCollection($collection); $res = $collection->count($criteria); \PHPUnit\Framework\Assert::assertLessThan(1, $res); } /** * Grabs a data from collection * * ``` php * <?php * $user = $I->grabFromCollection('users', array('name' => 'miles')); * ``` * * @param $collection * @param array $criteria * @return array */ public function grabFromCollection($collection, $criteria = []) { $collection = $this->driver->getDbh()->selectCollection($collection); return $collection->findOne($criteria); } /** * Grabs the documents count from a collection * * ``` php * <?php * $count = $I->grabCollectionCount('users'); * // or * $count = $I->grabCollectionCount('users', array('isAdmin' => true)); * ``` * * @param $collection * @param array $criteria * @return integer */ public function grabCollectionCount($collection, $criteria = []) { $collection = $this->driver->getDbh()->selectCollection($collection); return $collection->count($criteria); } /** * Asserts that an element in a collection exists and is an Array * * ``` php * <?php * $I->seeElementIsArray('users', array('name' => 'John Doe') , 'data.skills'); * ``` * * @param String $collection * @param Array $criteria * @param String $elementToCheck */ public function seeElementIsArray($collection, $criteria = [], $elementToCheck = null) { $collection = $this->driver->getDbh()->selectCollection($collection); $res = $collection->count( array_merge( $criteria, [ $elementToCheck => ['$exists' => true], '$where' => "Array.isArray(this.{$elementToCheck})" ] ) ); if ($res > 1) { throw new \PHPUnit\Framework\ExpectationFailedException( 'Error: you should test against a single element criteria when asserting that elementIsArray' ); } \PHPUnit\Framework\Assert::assertEquals(1, $res, 'Specified element is not a Mongo Object'); } /** * Asserts that an element in a collection exists and is an Object * * ``` php * <?php * $I->seeElementIsObject('users', array('name' => 'John Doe') , 'data'); * ``` * * @param String $collection * @param Array $criteria * @param String $elementToCheck */ public function seeElementIsObject($collection, $criteria = [], $elementToCheck = null) { $collection = $this->driver->getDbh()->selectCollection($collection); $res = $collection->count( array_merge( $criteria, [ $elementToCheck => ['$exists' => true], '$where' => "! Array.isArray(this.{$elementToCheck}) && isObject(this.{$elementToCheck})" ] ) ); if ($res > 1) { throw new \PHPUnit\Framework\ExpectationFailedException( 'Error: you should test against a single element criteria when asserting that elementIsObject' ); } \PHPUnit\Framework\Assert::assertEquals(1, $res, 'Specified element is not a Mongo Object'); } /** * Count number of records in a collection * * ``` php * <?php * $I->seeNumElementsInCollection('users', 2); * $I->seeNumElementsInCollection('users', 1, array('name' => 'miles')); * ``` * * @param $collection * @param integer $expected * @param array $criteria */ public function seeNumElementsInCollection($collection, $expected, $criteria = []) { $collection = $this->driver->getDbh()->selectCollection($collection); $res = $collection->count($criteria); \PHPUnit\Framework\Assert::assertSame($expected, $res); } /** * Returns list of classes and corresponding packages required for this module */ public function _requires() { return ['MongoDB\Client' => '"mongodb/mongodb": "^1.0"']; } } <?php namespace Codeception\Module; use Phalcon\Di; use PDOException; use Phalcon\Mvc\Url; use Phalcon\DiInterface; use Phalcon\Di\Injectable; use Codeception\TestInterface; use Codeception\Configuration; use Codeception\Lib\Framework; use Phalcon\Mvc\RouterInterface; use Phalcon\Mvc\Model as PhalconModel; use Phalcon\Mvc\Router\RouteInterface; use Codeception\Util\ReflectionHelper; use Codeception\Exception\ModuleException; use Codeception\Lib\Interfaces\ActiveRecord; use Codeception\Lib\Interfaces\PartedModule; use Codeception\Exception\ModuleConfigException; use Codeception\Lib\Connector\Phalcon as PhalconConnector; /** * This module provides integration with [Phalcon framework](http://www.phalconphp.com/) (3.x). * Please try it and leave your feedback. * * ## Demo Project * * <https://github.com/Codeception/phalcon-demo> * * ## Status * * * Maintainer: **Serghei Iakovlev** * * Stability: **stable** * * Contact: serghei@phalconphp.com * * ## Config * * The following configurations are required for this module: * * * bootstrap: `string`, default `app/config/bootstrap.php` - relative path to app.php config file * * cleanup: `boolean`, default `true` - all database queries will be run in a transaction, * which will be rolled back at the end of each test * * savepoints: `boolean`, default `true` - use savepoints to emulate nested transactions * * The application bootstrap file must return Application object but not call its handle() method. * * ## API * * * di - `Phalcon\Di\Injectable` instance * * client - `BrowserKit` client * * ## Parts * * By default all available methods are loaded, but you can specify parts to select only needed * actions and avoid conflicts. * * * `orm` - include only `haveRecord/grabRecord/seeRecord/dontSeeRecord` actions. * * `services` - allows to use `grabServiceFromContainer` and `addServiceToContainer`. * * Usage example: * * Sample bootstrap (`app/config/bootstrap.php`): * * ``` php * <?php * $config = include __DIR__ . "/config.php"; * include __DIR__ . "/loader.php"; * $di = new \Phalcon\DI\FactoryDefault(); * include __DIR__ . "/services.php"; * return new \Phalcon\Mvc\Application($di); * ?> * ``` * * ```yaml * actor: AcceptanceTester * modules: * enabled: * - Phalcon: * part: services * bootstrap: 'app/config/bootstrap.php' * cleanup: true * savepoints: true * - WebDriver: * url: http://your-url.com * browser: phantomjs * ``` */ class Phalcon extends Framework implements ActiveRecord, PartedModule { protected $config = [ 'bootstrap' => 'app/config/bootstrap.php', 'cleanup' => true, 'savepoints' => true, 'session' => PhalconConnector\MemorySession::class ]; /** * Phalcon bootstrap file path */ protected $bootstrapFile = null; /** * Dependency injection container * @var DiInterface */ public $di = null; /** * Phalcon Connector * @var PhalconConnector */ public $client; /** * HOOK: used after configuration is loaded * * @throws ModuleConfigException */ public function _initialize() { $this->bootstrapFile = Configuration::projectDir() . $this->config['bootstrap']; if (!file_exists($this->bootstrapFile)) { throw new ModuleConfigException( __CLASS__, "Bootstrap file does not exist in " . $this->config['bootstrap'] . "\n" . "Please create the bootstrap file that returns Application object\n" . "And specify path to it with 'bootstrap' config\n\n" . "Sample bootstrap: \n\n<?php\n" . '$config = include __DIR__ . "/config.php";' . "\n" . 'include __DIR__ . "/loader.php";' . "\n" . '$di = new \Phalcon\DI\FactoryDefault();' . "\n" . 'include __DIR__ . "/services.php";' . "\n" . 'return new \Phalcon\Mvc\Application($di);' ); } $this->client = new PhalconConnector(); } /** * HOOK: before scenario * * @param TestInterface $test * @throws ModuleException */ public function _before(TestInterface $test) { /** @noinspection PhpIncludeInspection */ $application = require $this->bootstrapFile; if (!$application instanceof Injectable) { throw new ModuleException(__CLASS__, 'Bootstrap must return \Phalcon\Di\Injectable object'); } $this->di = $application->getDI(); Di::reset(); Di::setDefault($this->di); if ($this->di->has('session')) { // Destroy existing sessions of previous tests $this->di['session'] = $this->di->get($this->config['session']); } if ($this->di->has('cookies')) { $this->di['cookies']->useEncryption(false); } if ($this->config['cleanup'] && $this->di->has('db')) { if ($this->config['savepoints']) { $this->di['db']->setNestedTransactionsWithSavepoints(true); } $this->di['db']->begin(); $this->debugSection('Database', 'Transaction started'); } // localize $bootstrap = $this->bootstrapFile; $this->client->setApplication(function () use ($bootstrap) { $currentDi = Di::getDefault(); /** @noinspection PhpIncludeInspection */ $application = require $bootstrap; $di = $application->getDI(); if ($currentDi->has('db')) { $di['db'] = $currentDi['db']; } if ($currentDi->has('session')) { $di['session'] = $currentDi['session']; } if ($di->has('cookies')) { $di['cookies']->useEncryption(false); } return $application; }); } /** * HOOK: after scenario * * @param TestInterface $test */ public function _after(TestInterface $test) { if ($this->config['cleanup'] && isset($this->di['db'])) { while ($this->di['db']->isUnderTransaction()) { $level = $this->di['db']->getTransactionLevel(); try { $this->di['db']->rollback(true); $this->debugSection('Database', 'Transaction cancelled; all changes reverted.'); } catch (PDOException $e) { } if ($level == $this->di['db']->getTransactionLevel()) { break; } } $this->di['db']->close(); } $this->di = null; Di::reset(); $_SESSION = $_FILES = $_GET = $_POST = $_COOKIE = $_REQUEST = []; } public function _parts() { return ['orm', 'services']; } /** * Provides access the Phalcon application object. * * @see \Codeception\Lib\Connector\Phalcon::getApplication * @return \Phalcon\Application|\Phalcon\Mvc\Micro */ public function getApplication() { return $this->client->getApplication(); } /** * Sets value to session. Use for authorization. * * @param string $key * @param mixed $val */ public function haveInSession($key, $val) { $this->di->get('session')->set($key, $val); $this->debugSection('Session', json_encode($this->di['session']->toArray())); } /** * Checks that session contains value. * If value is `null` checks that session has key. * * ``` php * <?php * $I->seeInSession('key'); * $I->seeInSession('key', 'value'); * ?> * ``` * * @param string $key * @param mixed $value */ public function seeInSession($key, $value = null) { $this->debugSection('Session', json_encode($this->di['session']->toArray())); if (is_array($key)) { $this->seeSessionHasValues($key); return; } if (!$this->di['session']->has($key)) { $this->fail("No session variable with key '$key'"); } if (is_null($value)) { $this->assertTrue($this->di['session']->has($key)); } else { $this->assertEquals($value, $this->di['session']->get($key)); } } /** * Assert that the session has a given list of values. * * ``` php * <?php * $I->seeSessionHasValues(['key1', 'key2']); * $I->seeSessionHasValues(['key1' => 'value1', 'key2' => 'value2']); * ?> * ``` * * @param array $bindings * @return void */ public function seeSessionHasValues(array $bindings) { foreach ($bindings as $key => $value) { if (is_int($key)) { $this->seeInSession($value); } else { $this->seeInSession($key, $value); } } } /** * Inserts record into the database. * * ``` php * <?php * $user_id = $I->haveRecord('App\Models\Users', ['name' => 'Phalcon']); * $I->haveRecord('App\Models\Categories', ['name' => 'Testing']'); * ?> * ``` * * @param string $model Model name * @param array $attributes Model attributes * @return mixed * @part orm */ public function haveRecord($model, $attributes = []) { $record = $this->getModelRecord($model); $res = $record->save($attributes); $field = function ($field) { if (is_array($field)) { return implode(', ', $field); } return $field; }; if (!$res) { $messages = $record->getMessages(); $errors = []; foreach ($messages as $message) { /** @var \Phalcon\Mvc\Model\MessageInterface $message */ $errors[] = sprintf( '[%s] %s: %s', $message->getType(), $field($message->getField()), $message->getMessage() ); } $this->fail(sprintf("Record %s was not saved. Messages: \n%s", $model, implode(PHP_EOL, $errors))); return null; } $this->debugSection($model, json_encode($record)); return $this->getModelIdentity($record); } /** * Checks that record exists in database. * * ``` php * <?php * $I->seeRecord('App\Models\Categories', ['name' => 'Testing']); * ?> * ``` * * @param string $model Model name * @param array $attributes Model attributes * @part orm */ public function seeRecord($model, $attributes = []) { $record = $this->findRecord($model, $attributes); if (!$record) { $this->fail("Couldn't find $model with " . json_encode($attributes)); } $this->debugSection($model, json_encode($record)); } /** * Checks that number of records exists in database. * * ``` php * <?php * $I->seeNumberOfRecords('App\Models\Categories', 3, ['name' => 'Testing']); * ?> * ``` * * @param string $model Model name * @param int $number int number of records * @param array $attributes Model attributes * @part orm */ public function seeNumberOfRecords($model, $number, $attributes = []) { $records = $this->findRecords($model, $attributes); if ($records->count() != $number) { $this->fail(sprintf( "Couldn't find %s records of %s with %s. Found: %s records.", $number, $model, json_encode($attributes), $records->count() )); } $this->debugSection($model, json_encode($records)); } /** * Checks that record does not exist in database. * * ``` php * <?php * $I->dontSeeRecord('App\Models\Categories', ['name' => 'Testing']); * ?> * ``` * * @param string $model Model name * @param array $attributes Model attributes * @part orm */ public function dontSeeRecord($model, $attributes = []) { $record = $this->findRecord($model, $attributes); $this->debugSection($model, json_encode($record)); if ($record) { $this->fail("Unexpectedly managed to find $model with " . json_encode($attributes)); } } /** * Retrieves record from database * * ``` php * <?php * $category = $I->grabRecord('App\Models\Categories', ['name' => 'Testing']); * ?> * ``` * * @param string $model Model name * @param array $attributes Model attributes * @return mixed * @part orm */ public function grabRecord($model, $attributes = []) { return $this->findRecord($model, $attributes); } /** * Resolves the service based on its configuration from Phalcon's DI container * Recommended to use for unit testing. * * @param string $service Service name * @param array $parameters Parameters [Optional] * @return mixed * @part services */ public function grabServiceFromContainer($service, array $parameters = []) { if (!$this->di->has($service)) { $this->fail("Service $service is not available in container"); } return $this->di->get($service, $parameters); } /** * Registers a service in the services container and resolve it. This record will be erased after the test. * Recommended to use for unit testing. * * ``` php * <?php * $filter = $I->addServiceToContainer('filter', ['className' => '\Phalcon\Filter']); * $filter = $I->addServiceToContainer('answer', function () { * return rand(0, 1) ? 'Yes' : 'No'; * }, true); * ?> * ``` * * @param string $name * @param mixed $definition * @param boolean $shared * @return mixed|null * @part services */ public function addServiceToContainer($name, $definition, $shared = false) { try { $service = $this->di->set($name, $definition, $shared); return $service->resolve(); } catch (\Exception $e) { $this->fail($e->getMessage()); return null; } } /** * Opens web page using route name and parameters. * * ``` php * <?php * $I->amOnRoute('posts.create'); * ?> * ``` * * @param string $routeName * @param array $params */ public function amOnRoute($routeName, array $params = []) { if (!$this->di->has('url')) { $this->fail('Unable to resolve "url" service.'); } /** @var Url $url */ $url = $this->di->getShared('url'); $urlParams = ['for' => $routeName]; if ($params) { $urlParams += $params; } $this->amOnPage($url->get($urlParams, null, true)); } /** * Checks that current url matches route * * ``` php * <?php * $I->seeCurrentRouteIs('posts.index'); * ?> * ``` * @param string $routeName */ public function seeCurrentRouteIs($routeName) { if (!$this->di->has('url')) { $this->fail('Unable to resolve "url" service.'); } /** @var Url $url */ $url = $this->di->getShared('url'); $this->seeCurrentUrlEquals($url->get(['for' => $routeName], null, true)); } /** * Allows to query the first record that match the specified conditions * * @param string $model Model name * @param array $attributes Model attributes * * @return \Phalcon\Mvc\Model */ protected function findRecord($model, $attributes = []) { $this->getModelRecord($model); $conditions = []; $bind = []; foreach ($attributes as $key => $value) { if ($value === null) { $conditions[] = "$key IS NULL"; } else { $conditions[] = "$key = :$key:"; $bind[$key] = $value; } } $query = implode(' AND ', $conditions); $this->debugSection('Query', $query); return call_user_func_array([$model, 'findFirst'], [ [ 'conditions' => $query, 'bind' => $bind, ] ]); } /** * Allows to query the many records that match the specified conditions * * @param string $model Model name * @param array $attributes Model attributes * * @return \Phalcon\Mvc\ResultsetInterface */ protected function findRecords($model, $attributes = []) { $this->getModelRecord($model); $conditions = []; $bind = []; foreach ($attributes as $key => $value) { if ($value === null) { $conditions[] = "$key IS NULL"; } else { $conditions[] = "$key = :$key:"; $bind[$key] = $value; } } $query = implode(' AND ', $conditions); $this->debugSection('Query', $query); return call_user_func_array([$model, 'find'], [ [ 'conditions' => $query, 'bind' => $bind, ] ]); } /** * Get Model Record * * @param $model * * @return \Phalcon\Mvc\Model * @throws ModuleException */ protected function getModelRecord($model) { if (!class_exists($model)) { throw new ModuleException(__CLASS__, "Model $model does not exist"); } $record = new $model; if (!$record instanceof PhalconModel) { throw new ModuleException(__CLASS__, "Model $model is not instance of \\Phalcon\\Mvc\\Model"); } return $record; } /** * Get identity. * * @param \Phalcon\Mvc\Model $model * @return mixed */ protected function getModelIdentity(PhalconModel $model) { if (property_exists($model, 'id')) { return $model->id; } if (!$this->di->has('modelsMetadata')) { return null; } $primaryKeys = $this->di->get('modelsMetadata')->getPrimaryKeyAttributes($model); switch (count($primaryKeys)) { case 0: return null; case 1: return $model->{$primaryKeys[0]}; default: return array_intersect_key(get_object_vars($model), array_flip($primaryKeys)); } } /** * Returns a list of recognized domain names * * @return array */ protected function getInternalDomains() { $internalDomains = [$this->getApplicationDomainRegex()]; /** @var RouterInterface $router */ $router = $this->di->get('router'); if ($router instanceof RouterInterface) { /** @var RouteInterface[] $routes */ $routes = $router->getRoutes(); foreach ($routes as $route) { if ($route instanceof RouteInterface) { $hostName = $route->getHostname(); if (!empty($hostName)) { $internalDomains[] = '/^' . str_replace('.', '\.', $route->getHostname()) . '$/'; } } } } return array_unique($internalDomains); } /** * @return string */ private function getApplicationDomainRegex() { $server = ReflectionHelper::readPrivateProperty($this->client, 'server'); $domain = $server['HTTP_HOST']; return '/^' . str_replace('.', '\.', $domain) . '$/'; } } <?php namespace Codeception\Module; use Codeception\Lib\Connector\Guzzle; use Codeception\Lib\InnerBrowser; use Codeception\Lib\Interfaces\MultiSession; use Codeception\Lib\Interfaces\Remote; use Codeception\Lib\Interfaces\RequiresPackage; use Codeception\TestInterface; use Codeception\Util\Uri; use GuzzleHttp\Client as GuzzleClient; /** * Uses [Guzzle](http://guzzlephp.org/) to interact with your application over CURL. * Module works over CURL and requires **PHP CURL extension** to be enabled. * * Use to perform web acceptance tests with non-javascript browser. * * If test fails stores last shown page in 'output' dir. * * ## Status * * * Maintainer: **davert** * * Stability: **stable** * * Contact: codeception@codeception.com * * * ## Configuration * * * url *required* - start url of your app * * headers - default headers are set before each test. * * handler (default: curl) - Guzzle handler to use. By default curl is used, also possible to pass `stream`, or any valid class name as [Handler](http://docs.guzzlephp.org/en/latest/handlers-and-middleware.html#handlers). * * middleware - Guzzle middlewares to add. An array of valid callables is required. * * curl - curl options * * cookies - ... * * auth - ... * * verify - ... * * .. those and other [Guzzle Request options](http://docs.guzzlephp.org/en/latest/request-options.html) * * * ### Example (`acceptance.suite.yml`) * * modules: * enabled: * - PhpBrowser: * url: 'http://localhost' * auth: ['admin', '123345'] * curl: * CURLOPT_RETURNTRANSFER: true * cookies: * cookie-1: * Name: userName * Value: john.doe * cookie-2: * Name: authToken * Value: 1abcd2345 * Domain: subdomain.domain.com * Path: /admin/ * Expires: 1292177455 * Secure: true * HttpOnly: false * * * All SSL certification checks are disabled by default. * Use Guzzle request options to configure certifications and others. * * ## Public API * * Those properties and methods are expected to be used in Helper classes: * * Properties: * * * `guzzle` - contains [Guzzle](http://guzzlephp.org/) client instance: `\GuzzleHttp\Client` * * `client` - Symfony BrowserKit instance. * */ class PhpBrowser extends InnerBrowser implements Remote, MultiSession, RequiresPackage { protected $requiredFields = ['url']; protected $config = [ 'headers' => [], 'verify' => false, 'expect' => false, 'timeout' => 30, 'curl' => [], 'refresh_max_interval' => 10, 'handler' => 'curl', 'middleware' => null, // required defaults (not recommended to change) 'allow_redirects' => false, 'http_errors' => false, 'cookies' => true, ]; protected $guzzleConfigFields = [ 'auth', 'proxy', 'verify', 'cert', 'query', 'ssl_key', 'proxy', 'expect', 'version', 'timeout', 'connect_timeout' ]; /** * @var \Codeception\Lib\Connector\Guzzle */ public $client; /** * @var GuzzleClient */ public $guzzle; public function _requires() { return ['GuzzleHttp\Client' => '"guzzlehttp/guzzle": ">=6.3.0 <7.0"']; } public function _initialize() { $this->_initializeSession(); } public function _before(TestInterface $test) { if (!$this->client) { $this->client = new Guzzle(); } $this->_prepareSession(); } public function _getUrl() { return $this->config['url']; } /** * Alias to `haveHttpHeader` * * @param $name * @param $value */ public function setHeader($name, $value) { $this->haveHttpHeader($name, $value); } public function amHttpAuthenticated($username, $password) { $this->client->setAuth($username, $password); } public function amOnUrl($url) { $host = Uri::retrieveHost($url); $config = $this->config; $config['url'] = $host; $this->_reconfigure($config); $page = substr($url, strlen($host)); if ($page === '') { $page = '/'; } $this->debugSection('Host', $host); $this->amOnPage($page); } public function amOnSubdomain($subdomain) { $url = $this->config['url']; $url = preg_replace('~(https?:\/\/)(.*\.)(.*\.)~', "$1$3", $url); // removing current subdomain $url = preg_replace('~(https?:\/\/)(.*)~', "$1$subdomain.$2", $url); // inserting new $config = $this->config; $config['url'] = $url; $this->_reconfigure($config); } protected function onReconfigure() { $this->_prepareSession(); } /** * Low-level API method. * If Codeception commands are not enough, use [Guzzle HTTP Client](http://guzzlephp.org/) methods directly * * Example: * * ``` php * <?php * $I->executeInGuzzle(function (\GuzzleHttp\Client $client) { * $client->get('/get', ['query' => ['foo' => 'bar']]); * }); * ?> * ``` * * It is not recommended to use this command on a regular basis. * If Codeception lacks important Guzzle Client methods, implement them and submit patches. * * @param callable $function */ public function executeInGuzzle(\Closure $function) { return $function($this->guzzle); } public function _getResponseCode() { return $this->getResponseStatusCode(); } public function _initializeSession() { // independent sessions need independent cookies $this->client = new Guzzle(); $this->_prepareSession(); } public function _prepareSession() { $defaults = array_intersect_key($this->config, array_flip($this->guzzleConfigFields)); $curlOptions = []; foreach ($this->config['curl'] as $key => $val) { if (defined($key)) { $curlOptions[constant($key)] = $val; } } $this->headers = $this->config['headers']; $this->setCookiesFromOptions(); $defaults['base_uri'] = $this->config['url']; $defaults['curl'] = $curlOptions; $handler = Guzzle::createHandler($this->config['handler']); if ($handler && is_array($this->config['middleware'])) { foreach ($this->config['middleware'] as $middleware) { $handler->push($middleware); } } $defaults['handler'] = $handler; $this->guzzle = new GuzzleClient($defaults); $this->client->setRefreshMaxInterval($this->config['refresh_max_interval']); $this->client->setClient($this->guzzle); } public function _backupSession() { return [ 'client' => $this->client, 'guzzle' => $this->guzzle, 'crawler' => $this->crawler, 'headers' => $this->headers, ]; } public function _loadSession($session) { foreach ($session as $key => $val) { $this->$key = $val; } } public function _closeSession($session = null) { unset($session); } } <?php namespace Codeception\Module; use Codeception\Module as CodeceptionModule; use Codeception\TestInterface; use Codeception\Exception\ModuleConfigException; use Codeception\Lib\Driver\AmazonSQS; use Codeception\Lib\Driver\Beanstalk; use Codeception\Lib\Driver\Iron; /** * * Works with Queue servers. * * Testing with a selection of remote/local queueing services, including Amazon's SQS service * Iron.io service and beanstalkd service. * * Supported and tested queue types are: * * * [Iron.io](http://iron.io/) * * [Beanstalkd](http://kr.github.io/beanstalkd/) * * [Amazon SQS](http://aws.amazon.com/sqs/) * * The following dependencies are needed for the listed queue servers: * * * Beanstalkd: pda/pheanstalk ~3.0 * * Amazon SQS: aws/aws-sdk-php * * IronMQ: iron-io/iron_mq * * ## Status * * * Stability: * - Iron.io: **stable** * - Beanstalkd: **stable** * - Amazon SQS: **stable** * * ## Config * * The configuration settings depending on which queueing service is being used, all the options are listed * here. Refer to the configuration examples below to identify the configuration options required for your chosen * service. * * * type - type of queueing server (defaults to beanstalkd). * * host - hostname/ip address of the queue server or the host for the iron.io when using iron.io service. * * port: 11300 - port number for the queue server. * * timeout: 90 - timeout settings for connecting the queue server. * * token - Iron.io access token. * * project - Iron.io project ID. * * key - AWS access key ID. * * version - AWS version (e.g. latest) * * endpoint - The full URI of the webservice. This is only required when connecting to a custom endpoint (e.g., a local version of SQS). * * secret - AWS secret access key. * Warning: * Hard-coding your credentials can be dangerous, because it is easy to accidentally commit your credentials * into an SCM repository, potentially exposing your credentials to more people than intended. * It can also make it difficult to rotate credentials in the future. * * profile - AWS credential profile * - it should be located in ~/.aws/credentials file * - eg: [default] * aws_access_key_id = YOUR_AWS_ACCESS_KEY_ID * aws_secret_access_key = YOUR_AWS_SECRET_ACCESS_KEY * [project1] * aws_access_key_id = YOUR_AWS_ACCESS_KEY_ID * aws_secret_access_key = YOUR_AWS_SECRET_ACCESS_KEY * - Note: Using IAM roles is the preferred technique for providing credentials * to applications running on Amazon EC2 * http://docs.aws.amazon.com/aws-sdk-php/v3/guide/guide/credentials.html?highlight=credentials * * * region - A region parameter is also required for AWS, refer to the AWS documentation for possible values list. * * ### Example * #### Example (beanstalkd) * * modules: * enabled: [Queue] * config: * Queue: * type: 'beanstalkd' * host: '127.0.0.1' * port: 11300 * timeout: 120 * * #### Example (Iron.io) * * modules: * enabled: [Queue] * config: * Queue: * 'type': 'iron', * 'host': 'mq-aws-us-east-1.iron.io', * 'token': 'your-token', * 'project': 'your-project-id' * * #### Example (AWS SQS) * * modules: * enabled: [Queue] * config: * Queue: * 'type': 'aws', * 'key': 'your-public-key', * 'secret': 'your-secret-key', * 'region': 'us-west-2' * * #### Example AWS SQS using profile credentials * * modules: * enabled: [Queue] * config: * Queue: * 'type': 'aws', * 'profile': 'project1', //see documentation * 'region': 'us-west-2' * * #### Example AWS SQS running on Amazon EC2 instance * * modules: * enabled: [Queue] * config: * Queue: * 'type': 'aws', * 'region': 'us-west-2' * */ class Queue extends CodeceptionModule { /** * @var \Codeception\Lib\Interfaces\Queue */ public $queueDriver; /** * Setup connection and open/setup the connection with config settings * * @param \Codeception\TestInterface $test */ public function _before(TestInterface $test) { $this->queueDriver->openConnection($this->config); } /** * Provide and override for the config settings and allow custom settings depending on the service being used. */ protected function validateConfig() { $this->queueDriver = $this->createQueueDriver(); $this->requiredFields = $this->queueDriver->getRequiredConfig(); $this->config = array_merge($this->queueDriver->getDefaultConfig(), $this->config); parent::validateConfig(); } /** * @return \Codeception\Lib\Interfaces\Queue * @throws ModuleConfigException */ protected function createQueueDriver() { switch ($this->config['type']) { case 'aws': case 'sqs': case 'aws_sqs': return new AmazonSQS(); case 'iron': case 'iron_mq': return new Iron(); case 'beanstalk': case 'beanstalkd': case 'beanstalkq': return new Beanstalk(); default: throw new ModuleConfigException( __CLASS__, "Unknown queue type {$this->config}; Supported queue types are: aws, iron, beanstalk" ); } } // ----------- SEARCH METHODS BELOW HERE ------------------------// /** * Check if a queue/tube exists on the queueing server. * * ```php * <?php * $I->seeQueueExists('default'); * ?> * ``` * * @param string $queue Queue Name */ public function seeQueueExists($queue) { $this->assertContains($queue, $this->queueDriver->getQueues()); } /** * Check if a queue/tube does NOT exist on the queueing server. * * ```php * <?php * $I->dontSeeQueueExists('default'); * ?> * ``` * * @param string $queue Queue Name */ public function dontSeeQueueExists($queue) { $this->assertNotContains($queue, $this->queueDriver->getQueues()); } /** * Check if a queue/tube is empty of all messages * * ```php * <?php * $I->seeEmptyQueue('default'); * ?> * ``` * * @param string $queue Queue Name */ public function seeEmptyQueue($queue) { $this->assertEquals(0, $this->queueDriver->getMessagesCurrentCountOnQueue($queue)); } /** * Check if a queue/tube is NOT empty of all messages * * ```php * <?php * $I->dontSeeEmptyQueue('default'); * ?> * ``` * * @param string $queue Queue Name */ public function dontSeeEmptyQueue($queue) { $this->assertNotEquals(0, $this->queueDriver->getMessagesCurrentCountOnQueue($queue)); } /** * Check if a queue/tube has a given current number of messages * * ```php * <?php * $I->seeQueueHasCurrentCount('default', 10); * ?> * ``` * * @param string $queue Queue Name * @param int $expected Number of messages expected */ public function seeQueueHasCurrentCount($queue, $expected) { $this->assertEquals($expected, $this->queueDriver->getMessagesCurrentCountOnQueue($queue)); } /** * Check if a queue/tube does NOT have a given current number of messages * * ```php * <?php * $I->dontSeeQueueHasCurrentCount('default', 10); * ?> * ``` * * @param string $queue Queue Name * @param int $expected Number of messages expected */ public function dontSeeQueueHasCurrentCount($queue, $expected) { $this->assertNotEquals($expected, $this->queueDriver->getMessagesCurrentCountOnQueue($queue)); } /** * Check if a queue/tube has a given total number of messages * * ```php * <?php * $I->seeQueueHasTotalCount('default', 10); * ?> * ``` * * @param string $queue Queue Name * @param int $expected Number of messages expected */ public function seeQueueHasTotalCount($queue, $expected) { $this->assertEquals($expected, $this->queueDriver->getMessagesTotalCountOnQueue($queue)); } /** * Check if a queue/tube does NOT have a given total number of messages * * ```php * <?php * $I->dontSeeQueueHasTotalCount('default', 10); * ?> * ``` * * @param string $queue Queue Name * @param int $expected Number of messages expected */ public function dontSeeQueueHasTotalCount($queue, $expected) { $this->assertNotEquals($expected, $this->queueDriver->getMessagesTotalCountOnQueue($queue)); } // ----------- UTILITY METHODS BELOW HERE -------------------------// /** * Add a message to a queue/tube * * ```php * <?php * $I->addMessageToQueue('this is a messages', 'default'); * ?> * ``` * * @param string $message Message Body * @param string $queue Queue Name */ public function addMessageToQueue($message, $queue) { $this->queueDriver->addMessageToQueue($message, $queue); } /** * Clear all messages of the queue/tube * * ```php * <?php * $I->clearQueue('default'); * ?> * ``` * * @param string $queue Queue Name */ public function clearQueue($queue) { $this->queueDriver->clearQueue($queue); } // ----------- GRABBER METHODS BELOW HERE -----------------------// /** * Grabber method to get the list of queues/tubes on the server * * ```php * <?php * $queues = $I->grabQueues(); * ?> * ``` * * @return array List of Queues/Tubes */ public function grabQueues() { return $this->queueDriver->getQueues(); } /** * Grabber method to get the current number of messages on the queue/tube (pending/ready) * * ```php * <?php * $I->grabQueueCurrentCount('default'); * ?> * ``` * @param string $queue Queue Name * * @return int Count */ public function grabQueueCurrentCount($queue) { return $this->queueDriver->getMessagesCurrentCountOnQueue($queue); } /** * Grabber method to get the total number of messages on the queue/tube * * ```php * <?php * $I->grabQueueTotalCount('default'); * ?> * ``` * * @param $queue Queue Name * * @return int Count */ public function grabQueueTotalCount($queue) { return $this->queueDriver->getMessagesTotalCountOnQueue($queue); } } <?php namespace Codeception\Module; use Codeception\Lib\Interfaces\RequiresPackage; use Codeception\Module as CodeceptionModule; use Codeception\Exception\ModuleException; use Codeception\TestInterface; use Predis\Client as RedisDriver; /** * This module uses the [Predis](https://github.com/nrk/predis) library * to interact with a Redis server. * * ## Status * * * Stability: **beta** * * ## Configuration * * * **`host`** (`string`, default `'127.0.0.1'`) - The Redis host * * **`port`** (`int`, default `6379`) - The Redis port * * **`database`** (`int`, no default) - The Redis database. Needs to be specified. * * **`cleanupBefore`**: (`string`, default `'never'`) - Whether/when to flush the database: * * `suite`: at the beginning of every suite * * `test`: at the beginning of every test * * Any other value: never * * ### Example (`unit.suite.yml`) * * ```yaml * modules: * - Redis: * host: '127.0.0.1' * port: 6379 * database: 0 * cleanupBefore: 'never' * ``` * * ## Public Properties * * * **driver** - Contains the Predis client/driver * * @author Marc Verney <marc@marcverney.net> */ class Redis extends CodeceptionModule implements RequiresPackage { /** * {@inheritdoc} * * No default value is set for the database, using this parameter. */ protected $config = [ 'host' => '127.0.0.1', 'port' => 6379, 'cleanupBefore' => 'never' ]; /** * {@inheritdoc} */ protected $requiredFields = [ 'database' ]; /** * The Redis driver * * @var RedisDriver */ public $driver; public function _requires() { return ['Predis\Client' => '"predis/predis": "^1.0"']; } /** * Instructions to run after configuration is loaded * * @throws ModuleException */ public function _initialize() { try { $this->driver = new RedisDriver([ 'host' => $this->config['host'], 'port' => $this->config['port'], 'database' => $this->config['database'] ]); } catch (\Exception $e) { throw new ModuleException( __CLASS__, $e->getMessage() ); } } /** * Code to run before each suite * * @param array $settings */ public function _beforeSuite($settings = []) { if ($this->config['cleanupBefore'] === 'suite') { $this->cleanup(); } } /** * Code to run before each test * * @param TestInterface $test */ public function _before(TestInterface $test) { if ($this->config['cleanupBefore'] === 'test') { $this->cleanup(); } } /** * Delete all the keys in the Redis database * * @throws ModuleException */ public function cleanup() { try { $this->driver->flushdb(); } catch (\Exception $e) { throw new ModuleException( __CLASS__, $e->getMessage() ); } } /** * Returns the value of a given key * * Examples: * * ``` php * <?php * // Strings * $I->grabFromRedis('string'); * * // Lists: get all members * $I->grabFromRedis('example:list'); * * // Lists: get a specific member * $I->grabFromRedis('example:list', 2); * * // Lists: get a range of elements * $I->grabFromRedis('example:list', 2, 4); * * // Sets: get all members * $I->grabFromRedis('example:set'); * * // ZSets: get all members * $I->grabFromRedis('example:zset'); * * // ZSets: get a range of members * $I->grabFromRedis('example:zset', 3, 12); * * // Hashes: get all fields of a key * $I->grabFromRedis('example:hash'); * * // Hashes: get a specific field of a key * $I->grabFromRedis('example:hash', 'foo'); * ``` * * @param string $key The key name * * @return mixed * * @throws ModuleException if the key does not exist */ public function grabFromRedis($key) { $args = func_get_args(); switch ($this->driver->type($key)) { case 'none': throw new ModuleException( $this, "Cannot grab key \"$key\" as it does not exist" ); break; case 'string': $reply = $this->driver->get($key); break; case 'list': if (count($args) === 2) { $reply = $this->driver->lindex($key, $args[1]); } else { $reply = $this->driver->lrange( $key, isset($args[1]) ? $args[1] : 0, isset($args[2]) ? $args[2] : -1 ); } break; case 'set': $reply = $this->driver->smembers($key); break; case 'zset': if (count($args) === 2) { throw new ModuleException( $this, "The method grabFromRedis(), when used with sorted " . "sets, expects either one argument or three" ); } $reply = $this->driver->zrange( $key, isset($args[2]) ? $args[1] : 0, isset($args[2]) ? $args[2] : -1, 'WITHSCORES' ); break; case 'hash': $reply = isset($args[1]) ? $this->driver->hget($key, $args[1]) : $this->driver->hgetall($key); break; default: $reply = null; } return $reply; } /** * Creates or modifies keys * * If $key already exists: * * - Strings: its value will be overwritten with $value * - Other types: $value items will be appended to its value * * Examples: * * ``` php * <?php * // Strings: $value must be a scalar * $I->haveInRedis('string', 'Obladi Oblada'); * * // Lists: $value can be a scalar or an array * $I->haveInRedis('list', ['riri', 'fifi', 'loulou']); * * // Sets: $value can be a scalar or an array * $I->haveInRedis('set', ['riri', 'fifi', 'loulou']); * * // ZSets: $value must be an associative array with scores * $I->haveInRedis('zset', ['riri' => 1, 'fifi' => 2, 'loulou' => 3]); * * // Hashes: $value must be an associative array * $I->haveInRedis('hash', ['obladi' => 'oblada']); * ``` * * @param string $type The type of the key * @param string $key The key name * @param mixed $value The value * * @throws ModuleException */ public function haveInRedis($type, $key, $value) { switch (strtolower($type)) { case 'string': if (!is_scalar($value)) { throw new ModuleException( $this, 'If second argument of haveInRedis() method is "string", ' . 'third argument must be a scalar' ); } $this->driver->set($key, $value); break; case 'list': $this->driver->rpush($key, $value); break; case 'set': $this->driver->sadd($key, $value); break; case 'zset': if (!is_array($value)) { throw new ModuleException( $this, 'If second argument of haveInRedis() method is "zset", ' . 'third argument must be an (associative) array' ); } $this->driver->zadd($key, $value); break; case 'hash': if (!is_array($value)) { throw new ModuleException( $this, 'If second argument of haveInRedis() method is "hash", ' . 'third argument must be an array' ); } $this->driver->hmset($key, $value); break; default: throw new ModuleException( $this, "Unknown type \"$type\" for key \"$key\". Allowed types are " . '"string", "list", "set", "zset", "hash"' ); } } /** * Asserts that a key does not exist or, optionally, that it doesn't have the * provided $value * * Examples: * * ``` php * <?php * // With only one argument, only checks the key does not exist * $I->dontSeeInRedis('example:string'); * * // Checks a String does not exist or its value is not the one provided * $I->dontSeeInRedis('example:string', 'life'); * * // Checks a List does not exist or its value is not the one provided (order of elements is compared). * $I->dontSeeInRedis('example:list', ['riri', 'fifi', 'loulou']); * * // Checks a Set does not exist or its value is not the one provided (order of members is ignored). * $I->dontSeeInRedis('example:set', ['riri', 'fifi', 'loulou']); * * // Checks a ZSet does not exist or its value is not the one provided (scores are required, order of members is compared) * $I->dontSeeInRedis('example:zset', ['riri' => 1, 'fifi' => 2, 'loulou' => 3]); * * // Checks a Hash does not exist or its value is not the one provided (order of members is ignored). * $I->dontSeeInRedis('example:hash', ['riri' => true, 'fifi' => 'Dewey', 'loulou' => 2]); * ``` * * @param string $key The key name * @param mixed $value Optional. If specified, also checks the key has this * value. Booleans will be converted to 1 and 0 (even inside arrays) */ public function dontSeeInRedis($key, $value = null) { $this->assertFalse( (bool) $this->checkKeyExists($key, $value), "The key \"$key\" exists" . ($value ? ' and its value matches the one provided' : '') ); } /** * Asserts that a given key does not contain a given item * * Examples: * * ``` php * <?php * // Strings: performs a substring search * $I->dontSeeRedisKeyContains('string', 'bar'); * * // Lists * $I->dontSeeRedisKeyContains('example:list', 'poney'); * * // Sets * $I->dontSeeRedisKeyContains('example:set', 'cat'); * * // ZSets: check whether the zset has this member * $I->dontSeeRedisKeyContains('example:zset', 'jordan'); * * // ZSets: check whether the zset has this member with this score * $I->dontSeeRedisKeyContains('example:zset', 'jordan', 23); * * // Hashes: check whether the hash has this field * $I->dontSeeRedisKeyContains('example:hash', 'magic'); * * // Hashes: check whether the hash has this field with this value * $I->dontSeeRedisKeyContains('example:hash', 'magic', 32); * ``` * * @param string $key The key * @param mixed $item The item * @param null $itemValue Optional and only used for zsets and hashes. If * specified, the method will also check that the $item has this value/score * * @return bool */ public function dontSeeRedisKeyContains($key, $item, $itemValue = null) { $this->assertFalse( (bool) $this->checkKeyContains($key, $item, $itemValue), "The key \"$key\" contains " . ( is_null($itemValue) ? "\"$item\"" : "[\"$item\" => \"$itemValue\"]" ) ); } /** * Asserts that a key exists, and optionally that it has the provided $value * * Examples: * * ``` php * <?php * // With only one argument, only checks the key exists * $I->seeInRedis('example:string'); * * // Checks a String exists and has the value "life" * $I->seeInRedis('example:string', 'life'); * * // Checks the value of a List. Order of elements is compared. * $I->seeInRedis('example:list', ['riri', 'fifi', 'loulou']); * * // Checks the value of a Set. Order of members is ignored. * $I->seeInRedis('example:set', ['riri', 'fifi', 'loulou']); * * // Checks the value of a ZSet. Scores are required. Order of members is compared. * $I->seeInRedis('example:zset', ['riri' => 1, 'fifi' => 2, 'loulou' => 3]); * * // Checks the value of a Hash. Order of members is ignored. * $I->seeInRedis('example:hash', ['riri' => true, 'fifi' => 'Dewey', 'loulou' => 2]); * ``` * * @param string $key The key name * @param mixed $value Optional. If specified, also checks the key has this * value. Booleans will be converted to 1 and 0 (even inside arrays) */ public function seeInRedis($key, $value = null) { $this->assertTrue( (bool) $this->checkKeyExists($key, $value), "Cannot find key \"$key\"" . ($value ? ' with the provided value' : '') ); } /** * Sends a command directly to the Redis driver. See documentation at * https://github.com/nrk/predis * Every argument that follows the $command name will be passed to it. * * Examples: * * ``` php * <?php * $I->sendCommandToRedis('incr', 'example:string'); * $I->sendCommandToRedis('strLen', 'example:string'); * $I->sendCommandToRedis('lPop', 'example:list'); * $I->sendCommandToRedis('zRangeByScore', 'example:set', '-inf', '+inf', ['withscores' => true, 'limit' => [1, 2]]); * $I->sendCommandToRedis('flushdb'); * ``` * * @param string $command The command name * * @return mixed */ public function sendCommandToRedis($command) { return call_user_func_array( [$this->driver, $command], array_slice(func_get_args(), 1) ); } /** * Asserts that a given key contains a given item * * Examples: * * ``` php * <?php * // Strings: performs a substring search * $I->seeRedisKeyContains('example:string', 'bar'); * * // Lists * $I->seeRedisKeyContains('example:list', 'poney'); * * // Sets * $I->seeRedisKeyContains('example:set', 'cat'); * * // ZSets: check whether the zset has this member * $I->seeRedisKeyContains('example:zset', 'jordan'); * * // ZSets: check whether the zset has this member with this score * $I->seeRedisKeyContains('example:zset', 'jordan', 23); * * // Hashes: check whether the hash has this field * $I->seeRedisKeyContains('example:hash', 'magic'); * * // Hashes: check whether the hash has this field with this value * $I->seeRedisKeyContains('example:hash', 'magic', 32); * ``` * * @param string $key The key * @param mixed $item The item * @param null $itemValue Optional and only used for zsets and hashes. If * specified, the method will also check that the $item has this value/score * * @return bool */ public function seeRedisKeyContains($key, $item, $itemValue = null) { $this->assertTrue( (bool) $this->checkKeyContains($key, $item, $itemValue), "The key \"$key\" does not contain " . ( is_null($itemValue) ? "\"$item\"" : "[\"$item\" => \"$itemValue\"]" ) ); } /** * Converts boolean values to "0" and "1" * * @param mixed $var The variable * * @return mixed */ private function boolToString($var) { $copy = is_array($var) ? $var : [$var]; foreach ($copy as $key => $value) { if (is_bool($value)) { $copy[$key] = $value ? '1' : '0'; } } return is_array($var) ? $copy : $copy[0]; } /** * Checks whether a key contains a given item * * @param string $key The key * @param mixed $item The item * @param null $itemValue Optional and only used for zsets and hashes. If * specified, the method will also check that the $item has this value/score * * @return bool * * @throws ModuleException */ private function checkKeyContains($key, $item, $itemValue = null) { $result = null; if (!is_scalar($item)) { throw new ModuleException( $this, "All arguments of [dont]seeRedisKeyContains() must be scalars" ); } switch ($this->driver->type($key)) { case 'string': $reply = $this->driver->get($key); $result = strpos($reply, $item) !== false; break; case 'list': $reply = $this->driver->lrange($key, 0, -1); $result = in_array($item, $reply); break; case 'set': $result = $this->driver->sismember($key, $item); break; case 'zset': $reply = $this->driver->zscore($key, $item); if (is_null($reply)) { $result = false; } elseif (!is_null($itemValue)) { $result = (float) $reply === (float) $itemValue; } else { $result = true; } break; case 'hash': $reply = $this->driver->hget($key, $item); $result = is_null($itemValue) ? !is_null($reply) : (string) $reply === (string) $itemValue; break; case 'none': throw new ModuleException( $this, "Key \"$key\" does not exist" ); break; } return $result; } /** * Checks whether a key exists and, optionally, whether it has a given $value * * @param string $key The key name * @param mixed $value Optional. If specified, also checks the key has this * value. Booleans will be converted to 1 and 0 (even inside arrays) * * @return bool */ private function checkKeyExists($key, $value = null) { $type = $this->driver->type($key); if (is_null($value)) { return $type != 'none'; } $value = $this->boolToString($value); switch ($type) { case 'string': $reply = $this->driver->get($key); // Allow non strict equality (2 equals '2') $result = $reply == $value; break; case 'list': $reply = $this->driver->lrange($key, 0, -1); // Check both arrays have the same key/value pairs + same order $result = $reply === $value; break; case 'set': $reply = $this->driver->smembers($key); // Only check both arrays have the same values sort($reply); sort($value); $result = $reply === $value; break; case 'zset': $reply = $this->driver->zrange($key, 0, -1, 'WITHSCORES'); // Check both arrays have the same key/value pairs + same order $reply = $this->scoresToFloat($reply); $value = $this->scoresToFloat($value); $result = $reply === $value; break; case 'hash': $reply = $this->driver->hgetall($key); // Only check both arrays have the same key/value pairs (==) $result = $reply == $value; break; default: $result = false; } return $result; } /** * Explicitly cast the scores of a Zset associative array as float/double * * @param array $arr The ZSet associative array * * @return array */ private function scoresToFloat(array $arr) { foreach ($arr as $member => $score) { $arr[$member] = (float) $score; } return $arr; } } <?php namespace Codeception\Module; use Codeception\Exception\ConfigurationException; use Codeception\Exception\ModuleConfigException; use Codeception\Exception\ModuleException; use Codeception\Lib\Interfaces\ConflictsWithModule; use Codeception\Module as CodeceptionModule; use Codeception\PHPUnit\Constraint\JsonContains; use Codeception\PHPUnit\Constraint\JsonType as JsonTypeConstraint; use Codeception\TestInterface; use Codeception\Lib\Interfaces\API; use Codeception\Lib\Framework; use Codeception\Lib\InnerBrowser; use Codeception\Lib\Interfaces\DependsOnModule; use Codeception\Lib\Interfaces\PartedModule; use Codeception\Util\JsonArray; use Codeception\Util\JsonType; use Codeception\Util\XmlStructure; use Codeception\Util\Soap as XmlUtils; /** * Module for testing REST WebService. * * This module can be used either with frameworks or PHPBrowser. * If a framework module is connected, the testing will occur in the application directly. * Otherwise, a PHPBrowser should be specified as a dependency to send requests and receive responses from a server. * * ## Configuration * * * url *optional* - the url of api * * shortDebugResponse *optional* - amount of chars to limit the api response length * * This module requires PHPBrowser or any of Framework modules enabled. * * ### Example * * modules: * enabled: * - REST: * depends: PhpBrowser * url: 'http://serviceapp/api/v1/' * shortDebugResponse: 300 # only the first 300 chars of the response * * ## Public Properties * * * headers - array of headers going to be sent. * * params - array of sent data * * response - last response (string) * * ## Parts * * * Json - actions for validating Json responses (no Xml responses) * * Xml - actions for validating XML responses (no Json responses) * * ## Conflicts * * Conflicts with SOAP module * */ class REST extends CodeceptionModule implements DependsOnModule, PartedModule, API, ConflictsWithModule { protected $config = [ 'url' => '', 'aws' => '' ]; protected $dependencyMessage = <<<EOF Example configuring PhpBrowser as backend for REST module. -- modules: enabled: - REST: depends: PhpBrowser url: http://localhost/api/ shortDebugResponse: 300 -- Framework modules can be used for testing of API as well. EOF; protected $DEFAULT_SHORTEN_VALUE = 150; /** * @var \Symfony\Component\HttpKernel\Client|\Symfony\Component\BrowserKit\Client */ public $client = null; public $isFunctional = false; /** * @var InnerBrowser */ protected $connectionModule; public $params = []; public $response = ""; public function _before(TestInterface $test) { $this->client = &$this->connectionModule->client; $this->resetVariables(); } protected function resetVariables() { $this->params = []; $this->response = ""; $this->connectionModule->headers = []; } public function _conflicts() { return 'Codeception\Lib\Interfaces\API'; } public function _depends() { return ['Codeception\Lib\InnerBrowser' => $this->dependencyMessage]; } public function _parts() { return ['xml', 'json']; } public function _inject(InnerBrowser $connection) { $this->connectionModule = $connection; if ($this->connectionModule instanceof Framework) { $this->isFunctional = true; } if ($this->connectionModule instanceof PhpBrowser) { if (!$this->connectionModule->_getConfig('url')) { $this->connectionModule->_setConfig(['url' => $this->config['url']]); } } } public function _failed(TestInterface $test, $fail) { if (!$this->response) { return; } $printedResponse = $this->response; if ($this->isBinaryData($printedResponse)) { $printedResponse = $this->binaryToDebugString($printedResponse); } $test->getMetadata()->addReport('body', $printedResponse); } protected function getRunningClient() { if ($this->client->getInternalRequest() === null) { throw new ModuleException($this, "Response is empty. Use `\$I->sendXXX()` methods to send HTTP request"); } return $this->client; } /** * Sets HTTP header valid for all next requests. Use `deleteHeader` to unset it * * ```php * <?php * $I->haveHttpHeader('Content-Type', 'application/json'); * // all next requests will contain this header * ?> * ``` * * @param $name * @param $value * @part json * @part xml */ public function haveHttpHeader($name, $value) { $this->connectionModule->haveHttpHeader($name, $value); } /** * Deletes the header with the passed name. Subsequent requests * will not have the deleted header in its request. * * Example: * ```php * <?php * $I->haveHttpHeader('X-Requested-With', 'Codeception'); * $I->sendGET('test-headers.php'); * // ... * $I->deleteHeader('X-Requested-With'); * $I->sendPOST('some-other-page.php'); * ?> * ``` * * @param string $name the name of the header to delete. * @part json * @part xml */ public function deleteHeader($name) { $this->connectionModule->deleteHeader($name); } /** * Checks over the given HTTP header and (optionally) * its value, asserting that are there * * @param $name * @param $value * @part json * @part xml */ public function seeHttpHeader($name, $value = null) { if ($value !== null) { $this->assertEquals( $value, $this->getRunningClient()->getInternalResponse()->getHeader($name) ); return; } $this->assertNotNull($this->getRunningClient()->getInternalResponse()->getHeader($name)); } /** * Checks over the given HTTP header and (optionally) * its value, asserting that are not there * * @param $name * @param $value * @part json * @part xml */ public function dontSeeHttpHeader($name, $value = null) { if ($value !== null) { $this->assertNotEquals( $value, $this->getRunningClient()->getInternalResponse()->getHeader($name) ); return; } $this->assertNull($this->getRunningClient()->getInternalResponse()->getHeader($name)); } /** * Checks that http response header is received only once. * HTTP RFC2616 allows multiple response headers with the same name. * You can check that you didn't accidentally sent the same header twice. * * ``` php * <?php * $I->seeHttpHeaderOnce('Cache-Control'); * ?>> * ``` * * @param $name * @part json * @part xml */ public function seeHttpHeaderOnce($name) { $headers = $this->getRunningClient()->getInternalResponse()->getHeader($name, false); $this->assertCount(1, $headers); } /** * Returns the value of the specified header name * * @param $name * @param Boolean $first Whether to return the first value or all header values * * @return string|array The first header value if $first is true, an array of values otherwise * @part json * @part xml */ public function grabHttpHeader($name, $first = true) { return $this->getRunningClient()->getInternalResponse()->getHeader($name, $first); } /** * Adds HTTP authentication via username/password. * * @param $username * @param $password * @part json * @part xml */ public function amHttpAuthenticated($username, $password) { if ($this->isFunctional) { $this->client->setServerParameter('PHP_AUTH_USER', $username); $this->client->setServerParameter('PHP_AUTH_PW', $password); } else { $this->client->setAuth($username, $password); } } /** * Adds Digest authentication via username/password. * * @param $username * @param $password * @part json * @part xml */ public function amDigestAuthenticated($username, $password) { if ($this->isFunctional) { throw new ModuleException(__METHOD__, 'Not supported by functional modules'); } $this->client->setAuth($username, $password, 'digest'); } /** * Adds Bearer authentication via access token. * * @param $accessToken * @part json * @part xml */ public function amBearerAuthenticated($accessToken) { $this->haveHttpHeader('Authorization', 'Bearer ' . $accessToken); } /** * Adds NTLM authentication via username/password. * Requires client to be Guzzle >=6.3.0 * Out of scope for functional modules. * * Example: * ```php * <?php * $I->amNTLMAuthenticated('jon_snow', 'targaryen'); * ?> * ``` * * @param $username * @param $password * @throws ModuleException * @part json * @part xml */ public function amNTLMAuthenticated($username, $password) { if ($this->isFunctional) { throw new ModuleException(__METHOD__, 'Not supported by functional modules'); } if (!defined('\GuzzleHttp\Client::VERSION')) { throw new ModuleException(__METHOD__, 'Not supported if not using a Guzzle client'); } if (version_compare(\GuzzleHttp\Client::VERSION, '6.2.1', 'lt')) { throw new ModuleException(__METHOD__, 'Guzzle ' . \GuzzleHttp\Client::VERSION . ' found. Requires Guzzle >=6.3.0 for NTLM auth option'); } $this->client->setAuth($username, $password, 'ntlm'); } /** * Allows to send REST request using AWS Authorization * * Only works with PhpBrowser * Example Config: * ```yml * modules: * enabled: * - REST: * aws: * key: accessKey * secret: accessSecret * service: awsService * region: awsRegion * ``` * Code: * ```php * <?php * $I->amAWSAuthenticated(); * ?> * ``` * @param array $additionalAWSConfig * @throws ModuleException */ public function amAWSAuthenticated($additionalAWSConfig = []) { if (method_exists($this->client, 'setAwsAuth')) { $config = array_merge($this->config['aws'], $additionalAWSConfig); if (!isset($config['key'])) { throw new ConfigurationException('AWS Key is not set'); } if (!isset($config['secret'])) { throw new ConfigurationException('AWS Secret is not set'); } if (!isset($config['service'])) { throw new ConfigurationException('AWS Service is not set'); } if (!isset($config['region'])) { throw new ConfigurationException('AWS Region is not set'); } $this->client->setAwsAuth($config); } } /** * Sends a POST request to given uri. Parameters and files can be provided separately. * * Example: * ```php * <?php * //simple POST call * $I->sendPOST('/message', ['subject' => 'Read this!', 'to' => 'johndoe@example.com']); * //simple upload method * $I->sendPOST('/message/24', ['inline' => 0], ['attachmentFile' => codecept_data_dir('sample_file.pdf')]); * //uploading a file with a custom name and mime-type. This is also useful to simulate upload errors. * $I->sendPOST('/message/24', ['inline' => 0], [ * 'attachmentFile' => [ * 'name' => 'document.pdf', * 'type' => 'application/pdf', * 'error' => UPLOAD_ERR_OK, * 'size' => filesize(codecept_data_dir('sample_file.pdf')), * 'tmp_name' => codecept_data_dir('sample_file.pdf') * ] * ]); * ``` * * @param $url * @param array|\JsonSerializable $params * @param array $files A list of filenames or "mocks" of $_FILES (each entry being an array with the following * keys: name, type, error, size, tmp_name (pointing to the real file path). Each key works * as the "name" attribute of a file input field. * * @see http://php.net/manual/en/features.file-upload.post-method.php * @see codecept_data_dir() * @part json * @part xml */ public function sendPOST($url, $params = [], $files = []) { $this->execute('POST', $url, $params, $files); } /** * Sends a HEAD request to given uri. * * @param $url * @param array $params * @part json * @part xml */ public function sendHEAD($url, $params = []) { $this->execute('HEAD', $url, $params); } /** * Sends an OPTIONS request to given uri. * * @param $url * @param array $params * @part json * @part xml */ public function sendOPTIONS($url, $params = []) { $this->execute('OPTIONS', $url, $params); } /** * Sends a GET request to given uri. * * @param $url * @param array $params * @part json * @part xml */ public function sendGET($url, $params = []) { $this->execute('GET', $url, $params); } /** * Sends PUT request to given uri. * * @param $url * @param array $params * @param array $files * @part json * @part xml */ public function sendPUT($url, $params = [], $files = []) { $this->execute('PUT', $url, $params, $files); } /** * Sends PATCH request to given uri. * * @param $url * @param array $params * @param array $files * @part json * @part xml */ public function sendPATCH($url, $params = [], $files = []) { $this->execute('PATCH', $url, $params, $files); } /** * Sends DELETE request to given uri. * * @param $url * @param array $params * @param array $files * @part json * @part xml */ public function sendDELETE($url, $params = [], $files = []) { $this->execute('DELETE', $url, $params, $files); } /** * Sets Headers "Link" as one header "Link" based on linkEntries * * @param array $linkEntries (entry is array with keys "uri" and "link-param") * * @link http://tools.ietf.org/html/rfc2068#section-19.6.2.4 * * @author samva.ua@gmail.com */ private function setHeaderLink(array $linkEntries) { $values = []; foreach ($linkEntries as $linkEntry) { \PHPUnit\Framework\Assert::assertArrayHasKey( 'uri', $linkEntry, 'linkEntry should contain property "uri"' ); \PHPUnit\Framework\Assert::assertArrayHasKey( 'link-param', $linkEntry, 'linkEntry should contain property "link-param"' ); $values[] = $linkEntry['uri'] . '; ' . $linkEntry['link-param']; } $this->haveHttpHeader('Link', implode(', ', $values)); } /** * Sends LINK request to given uri. * * @param $url * @param array $linkEntries (entry is array with keys "uri" and "link-param") * * @link http://tools.ietf.org/html/rfc2068#section-19.6.2.4 * * @author samva.ua@gmail.com * @part json * @part xml */ public function sendLINK($url, array $linkEntries) { $this->setHeaderLink($linkEntries); $this->execute('LINK', $url); } /** * Sends UNLINK request to given uri. * * @param $url * @param array $linkEntries (entry is array with keys "uri" and "link-param") * @link http://tools.ietf.org/html/rfc2068#section-19.6.2.4 * @author samva.ua@gmail.com * @part json * @part xml */ public function sendUNLINK($url, array $linkEntries) { $this->setHeaderLink($linkEntries); $this->execute('UNLINK', $url); } protected function execute($method, $url, $parameters = [], $files = []) { // allow full url to be requested if (!$url) { $url = $this->config['url']; } elseif (strpos($url, '://') === false && $this->config['url']) { $url = rtrim($this->config['url'], '/') . '/' . ltrim($url, '/'); } $this->params = $parameters; $parameters = $this->encodeApplicationJson($method, $parameters); if (is_array($parameters) || $method === 'GET') { if (!empty($parameters) && $method === 'GET') { if (strpos($url, '?') !== false) { $url .= '&'; } else { $url .= '?'; } $url .= http_build_query($parameters); } if ($method == 'GET') { $this->debugSection("Request", "$method $url"); $files = []; } else { $this->debugSection("Request", "$method $url " . json_encode($parameters)); $files = $this->formatFilesArray($files); } $this->response = (string)$this->connectionModule->_request($method, $url, $parameters, $files); } else { $requestData = $parameters; if ($this->isBinaryData($requestData)) { $requestData = $this->binaryToDebugString($requestData); } $this->debugSection("Request", "$method $url " . $requestData); $this->response = (string)$this->connectionModule->_request($method, $url, [], $files, [], $parameters); } $printedResponse = $this->response; if ($this->isBinaryData($printedResponse)) { $printedResponse = $this->binaryToDebugString($printedResponse); } $short = $this->_getConfig('shortDebugResponse'); if (!is_null($short)) { $printedResponse = $this->shortenMessage($printedResponse, $short); $this->debugSection("Shortened Response", $printedResponse); } else { $this->debugSection("Response", $printedResponse); } } /** * Check if data has non-printable bytes and it is not a valid unicode string * * @param string $data the text or binary data string * @return boolean */ protected function isBinaryData($data) { return !ctype_print($data) && false === mb_detect_encoding($data, mb_detect_order(), true); } /** * Format a binary string for debug printing * * @param string $data the binary data string * @return string the debug string */ protected function binaryToDebugString($data) { return '[binary-data length:' . strlen($data) . ' md5:' . md5($data) . ']'; } protected function encodeApplicationJson($method, $parameters) { if ($method !== 'GET' && array_key_exists('Content-Type', $this->connectionModule->headers) && ($this->connectionModule->headers['Content-Type'] === 'application/json' || preg_match('!^application/.+\+json$!', $this->connectionModule->headers['Content-Type']) ) ) { if ($parameters instanceof \JsonSerializable) { return json_encode($parameters); } if (is_array($parameters) || $parameters instanceof \ArrayAccess) { $parameters = $this->scalarizeArray($parameters); return json_encode($parameters); } } return $parameters; } private function formatFilesArray(array $files) { foreach ($files as $name => $value) { if (is_string($value)) { $this->checkFileBeforeUpload($value); $files[$name] = [ 'name' => basename($value), 'tmp_name' => $value, 'size' => filesize($value), 'type' => $this->getFileType($value), 'error' => 0, ]; continue; } elseif (is_array($value)) { if (isset($value['tmp_name'])) { $this->checkFileBeforeUpload($value['tmp_name']); if (!isset($value['name'])) { $value['name'] = basename($value['tmp_name']); } if (!isset($value['size'])) { $value['size'] = filesize($value['tmp_name']); } if (!isset($value['type'])) { $value['type'] = $this->getFileType($value['tmp_name']); } if (!isset($value['error'])) { $value['error'] = 0; } } else { $files[$name] = $this->formatFilesArray($value); } } elseif (is_object($value)) { /** * do nothing, probably the user knows what he is doing * @issue https://github.com/Codeception/Codeception/issues/3298 */ } else { throw new ModuleException(__CLASS__, "Invalid value of key $name in files array"); } } return $files; } private function getFileType($file) { if (function_exists('mime_content_type') && mime_content_type($file)) { return mime_content_type($file); } return 'application/octet-stream'; } private function checkFileBeforeUpload($file) { if (!file_exists($file)) { throw new ModuleException(__CLASS__, "File $file does not exist"); } if (!is_readable($file)) { throw new ModuleException(__CLASS__, "File $file is not readable"); } if (!is_file($file)) { throw new ModuleException(__CLASS__, "File $file is not a regular file"); } } /** * Extends the function Module::validateConfig for shorten messages * */ protected function validateConfig() { parent::validateConfig(); $short = $this->_getConfig('shortDebugResponse'); if (!is_null($short)) { if (!is_int($short) || $short < 0) { throw new ModuleConfigException(__CLASS__, 'The value of "shortDebugMessage" should be integer and greater or equal "0".'); } } } /** * Checks whether last response was valid JSON. * This is done with json_last_error function. * * @part json */ public function seeResponseIsJson() { $responseContent = $this->connectionModule->_getResponseContent(); \PHPUnit\Framework\Assert::assertNotEquals('', $responseContent, 'response is empty'); json_decode($responseContent); $errorCode = json_last_error(); $errorMessage = json_last_error_msg(); \PHPUnit\Framework\Assert::assertEquals( JSON_ERROR_NONE, $errorCode, sprintf( "Invalid json: %s. System message: %s.", $responseContent, $errorMessage ) ); } /** * Checks whether the last response contains text. * * @param $text * @part json * @part xml */ public function seeResponseContains($text) { $this->assertStringContainsString($text, $this->connectionModule->_getResponseContent(), "REST response contains"); } /** * Checks whether last response do not contain text. * * @param $text * @part json * @part xml */ public function dontSeeResponseContains($text) { $this->assertStringNotContainsString($text, $this->connectionModule->_getResponseContent(), "REST response contains"); } /** * Checks whether the last JSON response contains provided array. * The response is converted to array with json_decode($response, true) * Thus, JSON is represented by associative array. * This method matches that response array contains provided array. * * Examples: * * ``` php * <?php * // response: {name: john, email: john@gmail.com} * $I->seeResponseContainsJson(array('name' => 'john')); * * // response {user: john, profile: { email: john@gmail.com }} * $I->seeResponseContainsJson(array('email' => 'john@gmail.com')); * * ?> * ``` * * This method recursively checks if one array can be found inside of another. * * @param array $json * @part json */ public function seeResponseContainsJson($json = []) { \PHPUnit\Framework\Assert::assertThat( $this->connectionModule->_getResponseContent(), new JsonContains($json) ); } /** * Returns current response so that it can be used in next scenario steps. * * Example: * * ``` php * <?php * $user_id = $I->grabResponse(); * $I->sendPUT('/user', array('id' => $user_id, 'name' => 'davert')); * ?> * ``` * * @return string * @part json * @part xml * @version 1.1 */ public function grabResponse() { return $this->connectionModule->_getResponseContent(); } /** * Returns data from the current JSON response using [JSONPath](http://goessner.net/articles/JsonPath/) as selector. * JsonPath is XPath equivalent for querying Json structures. * Try your JsonPath expressions [online](http://jsonpath.curiousconcept.com/). * Even for a single value an array is returned. * * This method **require [`flow/jsonpath` > 0.2](https://github.com/FlowCommunications/JSONPath/) library to be installed**. * * Example: * * ``` php * <?php * // match the first `user.id` in json * $firstUserId = $I->grabDataFromResponseByJsonPath('$..users[0].id'); * $I->sendPUT('/user', array('id' => $firstUserId[0], 'name' => 'davert')); * ?> * ``` * * @param string $jsonPath * @return array Array of matching items * @throws \Exception * @part json * @version 2.0.9 */ public function grabDataFromResponseByJsonPath($jsonPath) { return (new JsonArray($this->connectionModule->_getResponseContent()))->filterByJsonPath($jsonPath); } /** * Checks if json structure in response matches the xpath provided. * JSON is not supposed to be checked against XPath, yet it can be converted to xml and used with XPath. * This assertion allows you to check the structure of response json. * * * ```json * { "store": { * "book": [ * { "category": "reference", * "author": "Nigel Rees", * "title": "Sayings of the Century", * "price": 8.95 * }, * { "category": "fiction", * "author": "Evelyn Waugh", * "title": "Sword of Honour", * "price": 12.99 * } * ], * "bicycle": { * "color": "red", * "price": 19.95 * } * } * } * ``` * * ```php * <?php * // at least one book in store has author * $I->seeResponseJsonMatchesXpath('//store/book/author'); * // first book in store has author * $I->seeResponseJsonMatchesXpath('//store/book[1]/author'); * // at least one item in store has price * $I->seeResponseJsonMatchesXpath('/store//price'); * ?> * ``` * @param string $xpath * @part json * @version 2.0.9 */ public function seeResponseJsonMatchesXpath($xpath) { $response = $this->connectionModule->_getResponseContent(); $this->assertGreaterThan( 0, (new JsonArray($response))->filterByXPath($xpath)->length, "Received JSON did not match the XPath `$xpath`.\nJson Response: \n" . $response ); } /** * Opposite to seeResponseJsonMatchesXpath * * @param string $xpath * @part json */ public function dontSeeResponseJsonMatchesXpath($xpath) { $response = $this->connectionModule->_getResponseContent(); $this->assertEquals( 0, (new JsonArray($response))->filterByXPath($xpath)->length, "Received JSON matched the XPath `$xpath`.\nJson Response: \n" . $response ); } /** * Checks if json structure in response matches [JsonPath](http://goessner.net/articles/JsonPath/). * JsonPath is XPath equivalent for querying Json structures. * Try your JsonPath expressions [online](http://jsonpath.curiousconcept.com/). * This assertion allows you to check the structure of response json. * * This method **require [`flow/jsonpath` > 0.2](https://github.com/FlowCommunications/JSONPath/) library to be installed**. * * ```json * { "store": { * "book": [ * { "category": "reference", * "author": "Nigel Rees", * "title": "Sayings of the Century", * "price": 8.95 * }, * { "category": "fiction", * "author": "Evelyn Waugh", * "title": "Sword of Honour", * "price": 12.99 * } * ], * "bicycle": { * "color": "red", * "price": 19.95 * } * } * } * ``` * * ```php * <?php * // at least one book in store has author * $I->seeResponseJsonMatchesJsonPath('$.store.book[*].author'); * // first book in store has author * $I->seeResponseJsonMatchesJsonPath('$.store.book[0].author'); * // at least one item in store has price * $I->seeResponseJsonMatchesJsonPath('$.store..price'); * ?> * ``` * * @param string $jsonPath * @part json * @version 2.0.9 */ public function seeResponseJsonMatchesJsonPath($jsonPath) { $response = $this->connectionModule->_getResponseContent(); $this->assertNotEmpty( (new JsonArray($response))->filterByJsonPath($jsonPath), "Received JSON did not match the JsonPath `$jsonPath`.\nJson Response: \n" . $response ); } /** * Opposite to seeResponseJsonMatchesJsonPath * * @param string $jsonPath * @part json */ public function dontSeeResponseJsonMatchesJsonPath($jsonPath) { $response = $this->connectionModule->_getResponseContent(); $this->assertEmpty( (new JsonArray($response))->filterByJsonPath($jsonPath), "Received JSON matched the JsonPath `$jsonPath`.\nJson Response: \n" . $response ); } /** * Opposite to seeResponseContainsJson * * @part json * @param array $json */ public function dontSeeResponseContainsJson($json = []) { $jsonResponseArray = new JsonArray($this->connectionModule->_getResponseContent()); $this->assertFalse( $jsonResponseArray->containsArray($json), "Response JSON contains provided JSON\n" . "- <info>" . var_export($json, true) . "</info>\n" . "+ " . var_export($jsonResponseArray->toArray(), true) ); } /** * Checks that Json matches provided types. * In case you don't know the actual values of JSON data returned you can match them by type. * Starts check with a root element. If JSON data is array it will check the first element of an array. * You can specify the path in the json which should be checked with JsonPath * * Basic example: * * ```php * <?php * // {'user_id': 1, 'name': 'davert', 'is_active': false} * $I->seeResponseMatchesJsonType([ * 'user_id' => 'integer', * 'name' => 'string|null', * 'is_active' => 'boolean' * ]); * * // narrow down matching with JsonPath: * // {"users": [{ "name": "davert"}, {"id": 1}]} * $I->seeResponseMatchesJsonType(['name' => 'string'], '$.users[0]'); * ?> * ``` * * In this case you can match that record contains fields with data types you expected. * The list of possible data types: * * * string * * integer * * float * * array (json object is array as well) * * boolean * * You can also use nested data type structures: * * ```php * <?php * // {'user_id': 1, 'name': 'davert', 'company': {'name': 'Codegyre'}} * $I->seeResponseMatchesJsonType([ * 'user_id' => 'integer|string', // multiple types * 'company' => ['name' => 'string'] * ]); * ?> * ``` * * You can also apply filters to check values. Filter can be applied with `:` char after the type declaration. * * Here is the list of possible filters: * * * `integer:>{val}` - checks that integer is greater than {val} (works with float and string types too). * * `integer:<{val}` - checks that integer is lower than {val} (works with float and string types too). * * `string:url` - checks that value is valid url. * * `string:date` - checks that value is date in JavaScript format: https://weblog.west-wind.com/posts/2014/Jan/06/JavaScript-JSON-Date-Parsing-and-real-Dates * * `string:email` - checks that value is a valid email according to http://emailregex.com/ * * `string:regex({val})` - checks that string matches a regex provided with {val} * * This is how filters can be used: * * ```php * <?php * // {'user_id': 1, 'email' => 'davert@codeception.com'} * $I->seeResponseMatchesJsonType([ * 'user_id' => 'string:>0:<1000', // multiple filters can be used * 'email' => 'string:regex(~\@~)' // we just check that @ char is included * ]); * * // {'user_id': '1'} * $I->seeResponseMatchesJsonType([ * 'user_id' => 'string:>0', // works with strings as well * } * ?> * ``` * * You can also add custom filters y accessing `JsonType::addCustomFilter` method. * See [JsonType reference](http://codeception.com/docs/reference/JsonType). * * @part json * @param array $jsonType * @param string $jsonPath * @version 2.1.3 */ public function seeResponseMatchesJsonType(array $jsonType, $jsonPath = null) { $jsonArray = new JsonArray($this->connectionModule->_getResponseContent()); if ($jsonPath) { $jsonArray = $jsonArray->filterByJsonPath($jsonPath); } \PHPUnit\Framework\Assert::assertThat($jsonArray, new JsonTypeConstraint($jsonType)); } /** * Opposite to `seeResponseMatchesJsonType`. * * @part json * @param $jsonType jsonType structure * @param null $jsonPath optionally set specific path to structure with JsonPath * @see seeResponseMatchesJsonType * @version 2.1.3 */ public function dontSeeResponseMatchesJsonType($jsonType, $jsonPath = null) { $jsonArray = new JsonArray($this->connectionModule->_getResponseContent()); if ($jsonPath) { $jsonArray = $jsonArray->filterByJsonPath($jsonPath); } \PHPUnit\Framework\Assert::assertThat($jsonArray, new JsonTypeConstraint($jsonType, false)); } /** * Checks if response is exactly the same as provided. * * @part json * @part xml * @param $response */ public function seeResponseEquals($expected) { $this->assertEquals($expected, $this->connectionModule->_getResponseContent()); } /** * Checks response code equals to provided value. * * ```php * <?php * $I->seeResponseCodeIs(200); * * // preferred to use \Codeception\Util\HttpCode * $I->seeResponseCodeIs(\Codeception\Util\HttpCode::OK); * ``` * * @part json * @part xml * @param $code */ public function seeResponseCodeIs($code) { $this->connectionModule->seeResponseCodeIs($code); } /** * Checks that response code is not equal to provided value. * * ```php * <?php * $I->dontSeeResponseCodeIs(200); * * // preferred to use \Codeception\Util\HttpCode * $I->dontSeeResponseCodeIs(\Codeception\Util\HttpCode::OK); * ``` * * @part json * @part xml * @param $code */ public function dontSeeResponseCodeIs($code) { $this->connectionModule->dontSeeResponseCodeIs($code); } /** * Checks that the response code is 2xx * * @part json * @part xml */ public function seeResponseCodeIsSuccessful() { $this->connectionModule->seeResponseCodeIsSuccessful(); } /** * Checks that the response code 3xx * * @part json * @part xml */ public function seeResponseCodeIsRedirection() { $this->connectionModule->seeResponseCodeIsRedirection(); } /** * Checks that the response code is 4xx * * @part json * @part xml */ public function seeResponseCodeIsClientError() { $this->connectionModule->seeResponseCodeIsClientError(); } /** * Checks that the response code is 5xx * * @part json * @part xml */ public function seeResponseCodeIsServerError() { $this->connectionModule->seeResponseCodeIsServerError(); } /** * Checks whether last response was valid XML. * This is done with libxml_get_last_error function. * * @part xml */ public function seeResponseIsXml() { libxml_use_internal_errors(true); $doc = simplexml_load_string($this->connectionModule->_getResponseContent()); $num = ""; $title = ""; if ($doc === false) { $error = libxml_get_last_error(); $num = $error->code; $title = trim($error->message); libxml_clear_errors(); } libxml_use_internal_errors(false); \PHPUnit\Framework\Assert::assertNotSame( false, $doc, "xml decoding error #$num with message \"$title\", see http://www.xmlsoft.org/html/libxml-xmlerror.html" ); } /** * Checks whether XML response matches XPath * * ```php * <?php * $I->seeXmlResponseMatchesXpath('//root/user[@id=1]'); * ``` * @part xml * @param $xpath */ public function seeXmlResponseMatchesXpath($xpath) { $structure = new XmlStructure($this->connectionModule->_getResponseContent()); $this->assertTrue($structure->matchesXpath($xpath), 'xpath not matched'); } /** * Checks whether XML response does not match XPath * * ```php * <?php * $I->dontSeeXmlResponseMatchesXpath('//root/user[@id=1]'); * ``` * @part xml * @param $xpath */ public function dontSeeXmlResponseMatchesXpath($xpath) { $structure = new XmlStructure($this->connectionModule->_getResponseContent()); $this->assertFalse($structure->matchesXpath($xpath), 'accidentally matched xpath'); } /** * Finds and returns text contents of element. * Element is matched by either CSS or XPath * * @param $cssOrXPath * @return string * @part xml */ public function grabTextContentFromXmlElement($cssOrXPath) { $el = (new XmlStructure($this->connectionModule->_getResponseContent()))->matchElement($cssOrXPath); return $el->textContent; } /** * Finds and returns attribute of element. * Element is matched by either CSS or XPath * * @param $cssOrXPath * @param $attribute * @return string * @part xml */ public function grabAttributeFromXmlElement($cssOrXPath, $attribute) { $el = (new XmlStructure($this->connectionModule->_getResponseContent()))->matchElement($cssOrXPath); if (!$el->hasAttribute($attribute)) { $this->fail("Attribute not found in element matched by '$cssOrXPath'"); } return $el->getAttribute($attribute); } /** * Checks XML response equals provided XML. * Comparison is done by canonicalizing both xml`s. * * Parameters can be passed either as DOMDocument, DOMNode, XML string, or array (if no attributes). * * @param $xml * @part xml */ public function seeXmlResponseEquals($xml) { \PHPUnit\Framework\Assert::assertXmlStringEqualsXmlString($this->connectionModule->_getResponseContent(), $xml); } /** * Checks XML response does not equal to provided XML. * Comparison is done by canonicalizing both xml`s. * * Parameter can be passed either as XmlBuilder, DOMDocument, DOMNode, XML string, or array (if no attributes). * * @param $xml * @part xml */ public function dontSeeXmlResponseEquals($xml) { \PHPUnit\Framework\Assert::assertXmlStringNotEqualsXmlString( $this->connectionModule->_getResponseContent(), $xml ); } /** * Checks XML response includes provided XML. * Comparison is done by canonicalizing both xml`s. * Parameter can be passed either as XmlBuilder, DOMDocument, DOMNode, XML string, or array (if no attributes). * * Example: * * ``` php * <?php * $I->seeXmlResponseIncludes("<result>1</result>"); * ?> * ``` * * @param $xml * @part xml */ public function seeXmlResponseIncludes($xml) { $this->assertStringContainsString( XmlUtils::toXml($xml)->C14N(), XmlUtils::toXml($this->connectionModule->_getResponseContent())->C14N(), "found in XML Response" ); } /** * Checks XML response does not include provided XML. * Comparison is done by canonicalizing both xml`s. * Parameter can be passed either as XmlBuilder, DOMDocument, DOMNode, XML string, or array (if no attributes). * * @param $xml * @part xml */ public function dontSeeXmlResponseIncludes($xml) { $this->assertStringNotContainsString( XmlUtils::toXml($xml)->C14N(), XmlUtils::toXml($this->connectionModule->_getResponseContent())->C14N(), "found in XML Response" ); } /** * Checks if the hash of a binary response is exactly the same as provided. * Parameter can be passed as any hash string supported by hash(), with an * optional second parameter to specify the hash type, which defaults to md5. * * Example: Using md5 hash key * * ```php * <?php * $I->seeBinaryResponseEquals("8c90748342f19b195b9c6b4eff742ded"); * ?> * ``` * * Example: Using md5 for a file contents * * ```php * <?php * $fileData = file_get_contents("test_file.jpg"); * $I->seeBinaryResponseEquals(md5($fileData)); * ?> * ``` * Example: Using sha256 hash * * ```php * <?php * $fileData = '/9j/2wBDAAMCAgICAgMCAgIDAwMDBAYEBAQEBAgGBgUGCQgKCgkICQkKDA8MCgsOCwkJDRENDg8QEBEQCgwSExIQEw8QEBD/yQALCAABAAEBAREA/8wABgAQEAX/2gAIAQEAAD8A0s8g/9k='; // very small jpeg * $I->seeBinaryResponseEquals(hash("sha256", base64_decode($fileData)), 'sha256'); * ?> * ``` * * @param $hash the hashed data response expected * @param $algo the hash algorithm to use. Default md5. * @part json * @part xml */ public function seeBinaryResponseEquals($hash, $algo = 'md5') { $responseHash = hash($algo, $this->connectionModule->_getResponseContent()); $this->assertEquals($hash, $responseHash); } /** * Checks if the hash of a binary response is not the same as provided. * * ```php * <?php * $I->dontSeeBinaryResponseEquals("8c90748342f19b195b9c6b4eff742ded"); * ?> * ``` * Opposite to `seeBinaryResponseEquals` * * @param $hash the hashed data response expected * @param $algo the hash algorithm to use. Default md5. * @part json * @part xml */ public function dontSeeBinaryResponseEquals($hash, $algo = 'md5') { $responseHash = hash($algo, $this->connectionModule->_getResponseContent()); $this->assertNotEquals($hash, $responseHash); } /** * Prevents automatic redirects to be followed by the client * * ```php * <?php * $I->stopFollowingRedirects(); * ``` * * @part xml * @part json */ public function stopFollowingRedirects() { $this->client->followRedirects(false); } /** * Enables automatic redirects to be followed by the client * * ```php * <?php * $I->startFollowingRedirects(); * ``` * * @part xml * @part json */ public function startFollowingRedirects() { $this->client->followRedirects(true); } } <?php namespace Codeception\Module; use Codeception\Module as CodeceptionModule; use Codeception\Exception\ModuleException; use Codeception\TestInterface; /** * Sequence solves data cleanup issue in alternative way. * Instead cleaning up the database between tests, * you can use generated unique names, that should not conflict. * When you create article on a site, for instance, you can assign it a unique name and then check it. * * This module has no actions, but introduces a function `sq` for generating unique sequences within test and * `sqs` for generating unique sequences across suite. * * ### Usage * * Function `sq` generates sequence, the only parameter it takes, is id. * You can get back to previously generated sequence using that id: * * ``` php * <?php * sq('post1'); // post1_521fbc63021eb * sq('post2'); // post2_521fbc6302266 * sq('post1'); // post1_521fbc63021eb * ``` * * Example: * * ``` php * <?php * $I->wantTo('create article'); * $I->click('New Article'); * $I->fillField('Title', sq('Article')); * $I->fillField('Body', 'Demo article with Lorem Ipsum'); * $I->click('save'); * $I->see(sq('Article') ,'#articles') * ``` * * Populating Database: * * ``` php * <?php * * for ($i = 0; $i<10; $i++) { * $I->haveInDatabase('users', array('login' => sq("user$i"), 'email' => sq("user$i").'@email.com'); * } * ?> * ``` * * Cest Suite tests: * * ``` php * <?php * class UserTest * { * public function createUser(AcceptanceTester $I) * { * $I->createUser(sqs('user') . '@mailserver.com', sqs('login'), sqs('pwd')); * } * * public function checkEmail(AcceptanceTester $I) * { * $I->seeInEmailTo(sqs('user') . '@mailserver.com', sqs('login')); * } * * public function removeUser(AcceptanceTester $I) * { * $I->removeUser(sqs('user') . '@mailserver.com'); * } * } * ?> * ``` * * ### Config * * By default produces unique string with param as a prefix: * * ``` * sq('user') => 'user_876asd8as87a' * ``` * * This behavior can be configured using `prefix` config param. * * Old style sequences: * * ```yaml * Sequence: * prefix: '_' * ``` * * Using id param inside prefix: * * ```yaml * Sequence: * prefix: '{id}.' * ``` */ class Sequence extends CodeceptionModule { public static $hash = []; public static $suiteHash = []; public static $prefix = ''; protected $config = ['prefix' => '{id}_']; public function _initialize() { static::$prefix = $this->config['prefix']; } public function _after(TestInterface $t) { self::$hash = []; } public function _afterSuite() { self::$suiteHash = []; } } if (!function_exists('sq') && !function_exists('sqs')) { require_once __DIR__ . '/../Util/sq.php'; } else { throw new ModuleException('Codeception\Module\Sequence', "function 'sq' and 'sqs' already defined"); } <?php namespace Codeception\Module; use Codeception\Lib\Interfaces\API; use Codeception\Lib\Interfaces\DependsOnModule; use Codeception\Lib\Notification; use Codeception\Module as CodeceptionModule; use Codeception\TestInterface; use Codeception\Exception\ModuleException; use Codeception\Exception\ModuleRequireException; use Codeception\Lib\Framework; use Codeception\Lib\InnerBrowser; use Codeception\Util\Soap as SoapUtils; use Codeception\Util\XmlStructure; /** * Module for testing SOAP WSDL web services. * Send requests and check if response matches the pattern. * * This module can be used either with frameworks or PHPBrowser. * It tries to guess the framework is is attached to. * If a endpoint is a full url then it uses PHPBrowser. * * ### Using Inside Framework * * Please note, that PHP SoapServer::handle method sends additional headers. * This may trigger warning: "Cannot modify header information" * If you use PHP SoapServer with framework, try to block call to this method in testing environment. * * ## Status * * * Maintainer: **davert** * * Stability: **stable** * * Contact: codecept@davert.mail.ua * * ## Configuration * * * endpoint *required* - soap wsdl endpoint * * SOAPAction - replace SOAPAction HTTP header (Set to '' to SOAP 1.2) * * ## Public Properties * * * xmlRequest - last SOAP request (DOMDocument) * * xmlResponse - last SOAP response (DOMDocument) * */ class SOAP extends CodeceptionModule implements DependsOnModule { protected $config = [ 'schema' => "", 'schema_url' => 'http://schemas.xmlsoap.org/soap/envelope/', 'framework_collect_buffer' => true ]; protected $requiredFields = ['endpoint']; protected $dependencyMessage = <<<EOF Example using PhpBrowser as backend for SOAP module. -- modules: enabled: - SOAP: depends: PhpBrowser -- Framework modules can be used as well for functional testing of SOAP API. EOF; /** * @var \Symfony\Component\BrowserKit\Client */ public $client = null; public $isFunctional = false; /** * @var \DOMDocument */ public $xmlRequest = null; /** * @var \DOMDocument */ public $xmlResponse = null; /** * @var XmlStructure */ protected $xmlStructure = null; /** * @var InnerBrowser */ protected $connectionModule; public function _before(TestInterface $test) { $this->client = &$this->connectionModule->client; $this->buildRequest(); $this->xmlResponse = null; $this->xmlStructure = null; } protected function onReconfigure() { $this->buildRequest(); $this->xmlResponse = null; $this->xmlStructure = null; } public function _depends() { return ['Codeception\Lib\InnerBrowser' => $this->dependencyMessage]; } public function _inject(InnerBrowser $connectionModule) { $this->connectionModule = $connectionModule; if ($connectionModule instanceof Framework) { $this->isFunctional = true; } } private function getClient() { if (!$this->client) { throw new ModuleRequireException($this, "Connection client is not available."); } return $this->client; } private function getXmlResponse() { if (!$this->xmlResponse) { throw new ModuleException($this, "No XML response, use `\$I->sendSoapRequest` to receive it"); } return $this->xmlResponse; } private function getXmlStructure() { if (!$this->xmlStructure) { $this->xmlStructure = new XmlStructure($this->getXmlResponse()); } return $this->xmlStructure; } /** * Prepare SOAP header. * Receives header name and parameters as array. * * Example: * * ``` php * <?php * $I->haveSoapHeader('AuthHeader', array('username' => 'davert', 'password' => '123345')); * ``` * * Will produce header: * * ``` * <soapenv:Header> * <SessionHeader> * <AuthHeader> * <username>davert</username> * <password>12345</password> * </AuthHeader> * </soapenv:Header> * ``` * * @param $header * @param array $params */ public function haveSoapHeader($header, $params = []) { $soap_schema_url = $this->config['schema_url']; $xml = $this->xmlRequest; $xmlHeader = $xml->documentElement->getElementsByTagNameNS($soap_schema_url, 'Header')->item(0); $headerEl = $xml->createElement($header); SoapUtils::arrayToXml($xml, $headerEl, $params); $xmlHeader->appendChild($headerEl); } /** * Submits request to endpoint. * * Requires of api function name and parameters. * Parameters can be passed either as DOMDocument, DOMNode, XML string, or array (if no attributes). * * You are allowed to execute as much requests as you need inside test. * * Example: * * ``` php * $I->sendSoapRequest('UpdateUser', '<user><id>1</id><name>notdavert</name></user>'); * $I->sendSoapRequest('UpdateUser', \Codeception\Utils\Soap::request()->user * ->id->val(1)->parent() * ->name->val('notdavert'); * ``` * * @param $request * @param $body */ public function sendSoapRequest($action, $body = "") { $soap_schema_url = $this->config['schema_url']; $xml = $this->xmlRequest; $call = $xml->createElement('ns:' . $action); if ($body) { $bodyXml = SoapUtils::toXml($body); if ($bodyXml->hasChildNodes()) { foreach ($bodyXml->childNodes as $bodyChildNode) { $bodyNode = $xml->importNode($bodyChildNode, true); $call->appendChild($bodyNode); } } } $xmlBody = $xml->getElementsByTagNameNS($soap_schema_url, 'Body')->item(0); // cleanup if body already set foreach ($xmlBody->childNodes as $node) { $xmlBody->removeChild($node); } $xmlBody->appendChild($call); $this->debugSection("Request", $req = $xml->C14N()); if ($this->isFunctional && $this->config['framework_collect_buffer']) { $response = $this->processInternalRequest($action, $req); } else { $response = $this->processExternalRequest($action, $req); } $this->debugSection("Response", (string) $response); $this->xmlResponse = SoapUtils::toXml($response); $this->xmlStructure = null; } /** * Checks XML response equals provided XML. * Comparison is done by canonicalizing both xml`s. * * Parameters can be passed either as DOMDocument, DOMNode, XML string, or array (if no attributes). * * Example: * * ``` php * <?php * $I->seeSoapResponseEquals("<?xml version="1.0" encoding="UTF-8"?><SOAP-ENV:Envelope><SOAP-ENV:Body><result>1</result></SOAP-ENV:Envelope>"); * * $dom = new \DOMDocument(); * $dom->load($file); * $I->seeSoapRequestIncludes($dom); * * ``` * * @param $xml */ public function seeSoapResponseEquals($xml) { $xml = SoapUtils::toXml($xml); $this->assertEquals($xml->C14N(), $this->getXmlResponse()->C14N()); } /** * Checks XML response includes provided XML. * Comparison is done by canonicalizing both xml`s. * Parameter can be passed either as XmlBuilder, DOMDocument, DOMNode, XML string, or array (if no attributes). * * Example: * * ``` php * <?php * $I->seeSoapResponseIncludes("<result>1</result>"); * $I->seeSoapRequestIncludes(\Codeception\Utils\Soap::response()->result->val(1)); * * $dom = new \DDOMDocument(); * $dom->load('template.xml'); * $I->seeSoapRequestIncludes($dom); * ?> * ``` * * @param $xml */ public function seeSoapResponseIncludes($xml) { $xml = $this->canonicalize($xml); $this->assertStringContainsString($xml, $this->getXmlResponse()->C14N(), "found in XML Response"); } /** * Checks XML response equals provided XML. * Comparison is done by canonicalizing both xml`s. * * Parameter can be passed either as XmlBuilder, DOMDocument, DOMNode, XML string, or array (if no attributes). * * @param $xml */ public function dontSeeSoapResponseEquals($xml) { $xml = SoapUtils::toXml($xml); \PHPUnit\Framework\Assert::assertXmlStringNotEqualsXmlString($xml->C14N(), $this->getXmlResponse()->C14N()); } /** * Checks XML response does not include provided XML. * Comparison is done by canonicalizing both xml`s. * Parameter can be passed either as XmlBuilder, DOMDocument, DOMNode, XML string, or array (if no attributes). * * @param $xml */ public function dontSeeSoapResponseIncludes($xml) { $xml = $this->canonicalize($xml); $this->assertStringNotContainsString($xml, $this->getXmlResponse()->C14N(), "found in XML Response"); } /** * Checks XML response contains provided structure. * Response elements will be compared with XML provided. * Only nodeNames are checked to see elements match. * * Example: * * ``` php * <?php * * $I->seeSoapResponseContainsStructure("<query><name></name></query>"); * ?> * ``` * * Use this method to check XML of valid structure is returned. * This method does not use schema for validation. * This method does not require path from root to match the structure. * * @param $xml */ public function seeSoapResponseContainsStructure($xml) { $xml = SoapUtils::toXml($xml); $this->debugSection("Structure", $xml->saveXML()); $this->assertTrue((bool)$this->getXmlStructure()->matchXmlStructure($xml), "this structure is in response"); } /** * Opposite to `seeSoapResponseContainsStructure` * @param $xml */ public function dontSeeSoapResponseContainsStructure($xml) { $xml = SoapUtils::toXml($xml); $this->debugSection("Structure", $xml->saveXML()); $this->assertFalse((bool)$this->getXmlStructure()->matchXmlStructure($xml), "this structure is in response"); } /** * Checks XML response with XPath locator * * ``` php * <?php * $I->seeSoapResponseContainsXPath('//root/user[@id=1]'); * ?> * ``` * * @param $xpath */ public function seeSoapResponseContainsXPath($xpath) { $this->assertTrue($this->getXmlStructure()->matchesXpath($xpath)); } /** * Checks XML response doesn't contain XPath locator * * ``` php * <?php * $I->dontSeeSoapResponseContainsXPath('//root/user[@id=1]'); * ?> * ``` * * @param $xpath */ public function dontSeeSoapResponseContainsXPath($xpath) { $this->assertFalse($this->getXmlStructure()->matchesXpath($xpath)); } /** * Checks response code from server. * * @param $code */ public function seeSoapResponseCodeIs($code) { $this->assertEquals( $code, $this->client->getInternalResponse()->getStatus(), "soap response code matches expected" ); } /** * Finds and returns text contents of element. * Element is matched by either CSS or XPath * * @version 1.1 * @param $cssOrXPath * @return string */ public function grabTextContentFrom($cssOrXPath) { $el = $this->getXmlStructure()->matchElement($cssOrXPath); return $el->textContent; } /** * Finds and returns attribute of element. * Element is matched by either CSS or XPath * * @version 1.1 * @param $cssOrXPath * @param $attribute * @return string */ public function grabAttributeFrom($cssOrXPath, $attribute) { $el = $this->getXmlStructure()->matchElement($cssOrXPath); if (!$el->hasAttribute($attribute)) { $this->fail("Attribute not found in element matched by '$cssOrXPath'"); } return $el->getAttribute($attribute); } protected function getSchema() { return $this->config['schema']; } protected function canonicalize($xml) { return SoapUtils::toXml($xml)->C14N(); } /** * @return \DOMDocument */ protected function buildRequest() { $soap_schema_url = $this->config['schema_url']; $xml = new \DOMDocument(); $root = $xml->createElement('soapenv:Envelope'); $xml->appendChild($root); $root->setAttribute('xmlns:ns', $this->getSchema()); $root->setAttribute('xmlns:soapenv', $soap_schema_url); $body = $xml->createElementNS($soap_schema_url, 'soapenv:Body'); $header = $xml->createElementNS($soap_schema_url, 'soapenv:Header'); $root->appendChild($header); $root->appendChild($body); $this->xmlRequest = $xml; return $xml; } protected function processRequest($action, $body) { $this->getClient()->request( 'POST', $this->config['endpoint'], [], [], [ 'HTTP_Content-Type' => 'text/xml; charset=UTF-8', 'HTTP_Content-Length' => strlen($body), 'HTTP_SOAPAction' => isset($this->config['SOAPAction']) ? $this->config['SOAPAction'] : $action ], $body ); } protected function processInternalRequest($action, $body) { ob_start(); try { $this->getClient()->setServerParameter('HTTP_HOST', 'localhost'); $this->processRequest($action, $body); } catch (\ErrorException $e) { // Zend_Soap outputs warning as an exception if (strpos($e->getMessage(), 'Warning: Cannot modify header information') === false) { ob_end_clean(); throw $e; } } $response = ob_get_contents(); ob_end_clean(); return $response; } protected function processExternalRequest($action, $body) { $this->processRequest($action, $body); return $this->client->getInternalResponse()->getContent(); } } <?php namespace Codeception\Module; use Codeception\Configuration; use Codeception\Exception\ModuleException; use Codeception\Lib\Framework; use Codeception\Exception\ModuleRequireException; use Codeception\Lib\Connector\Symfony as SymfonyConnector; use Codeception\Lib\Interfaces\DoctrineProvider; use Codeception\Lib\Interfaces\PartedModule; use Symfony\Component\Finder\Finder; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\Finder\SplFileInfo; use Symfony\Component\VarDumper\Cloner\Data; /** * This module uses Symfony Crawler and HttpKernel to emulate requests and test response. * * ## Demo Project * * <https://github.com/Codeception/symfony-demo> * * ## Config * * ### Symfony 4.x * * * app_path: 'src' - in Symfony 4 Kernel is located inside `src` * * environment: 'local' - environment used for load kernel * * kernel_class: 'App\Kernel' - kernel class name * * em_service: 'doctrine.orm.entity_manager' - use the stated EntityManager to pair with Doctrine Module. * * debug: true - turn on/off debug mode * * cache_router: 'false' - enable router caching between tests in order to [increase performance](http://lakion.com/blog/how-did-we-speed-up-sylius-behat-suite-with-blackfire) * * rebootable_client: 'true' - reboot client's kernel before each request * * #### Example (`functional.suite.yml`) - Symfony 4 Directory Structure * * modules: * enabled: * - Symfony: * app_path: 'src' * environment: 'test' * * * ### Symfony 3.x * * * app_path: 'app' - specify custom path to your app dir, where the kernel interface is located. * * var_path: 'var' - specify custom path to your var dir, where bootstrap cache is located. * * environment: 'local' - environment used for load kernel * * kernel_class: 'AppKernel' - kernel class name * * em_service: 'doctrine.orm.entity_manager' - use the stated EntityManager to pair with Doctrine Module. * * debug: true - turn on/off debug mode * * cache_router: 'false' - enable router caching between tests in order to [increase performance](http://lakion.com/blog/how-did-we-speed-up-sylius-behat-suite-with-blackfire) * * rebootable_client: 'true' - reboot client's kernel before each request * * #### Example (`functional.suite.yml`) - Symfony 3 Directory Structure * * modules: * enabled: * - Symfony: * app_path: 'app/front' * var_path: 'var' * environment: 'local_test' * * * ### Symfony 2.x * * * app_path: 'app' - specify custom path to your app dir, where bootstrap cache and kernel interface is located. * * environment: 'local' - environment used for load kernel * * kernel_class: 'AppKernel' - kernel class name * * debug: true - turn on/off debug mode * * em_service: 'doctrine.orm.entity_manager' - use the stated EntityManager to pair with Doctrine Module. * * cache_router: 'false' - enable router caching between tests in order to [increase performance](http://lakion.com/blog/how-did-we-speed-up-sylius-behat-suite-with-blackfire) * * rebootable_client: 'true' - reboot client's kernel before each request * * ### Example (`functional.suite.yml`) - Symfony 2.x Directory Structure * * ``` * modules: * - Symfony: * app_path: 'app/front' * environment: 'local_test' * ``` * * ## Public Properties * * * kernel - HttpKernel instance * * client - current Crawler instance * * ## Parts * * * services - allows to use Symfony DIC only with WebDriver or PhpBrowser modules. * * Usage example: * * ```yaml * actor: AcceptanceTester * modules: * enabled: * - Symfony: * part: SERVICES * - Doctrine2: * depends: Symfony * - WebDriver: * url: http://your-url.com * browser: phantomjs * ``` * */ class Symfony extends Framework implements DoctrineProvider, PartedModule { private static $possibleKernelClasses = [ 'AppKernel', // Symfony Standard 'App\Kernel', // Symfony Flex ]; /** * @var \Symfony\Component\HttpKernel\Kernel */ public $kernel; public $config = [ 'app_path' => 'app', 'var_path' => 'app', 'kernel_class' => null, 'environment' => 'test', 'debug' => true, 'cache_router' => false, 'em_service' => 'doctrine.orm.entity_manager', 'rebootable_client' => true, ]; /** * @return array */ public function _parts() { return ['services']; } /** * @var */ protected $kernelClass; /** * Services that should be persistent permanently for all tests * * @var array */ protected $permanentServices = []; /** * Services that should be persistent during test execution between kernel reboots * * @var array */ protected $persistentServices = []; public function _initialize() { $this->initializeSymfonyCache(); $this->kernelClass = $this->getKernelClass(); $maxNestingLevel = 200; // Symfony may have very long nesting level $xdebugMaxLevelKey = 'xdebug.max_nesting_level'; if (ini_get($xdebugMaxLevelKey) < $maxNestingLevel) { ini_set($xdebugMaxLevelKey, $maxNestingLevel); } $this->kernel = new $this->kernelClass($this->config['environment'], $this->config['debug']); $this->kernel->boot(); if ($this->config['cache_router'] === true) { $this->persistService('router', true); } } /** * Require Symfonys bootstrap.php.cache only for PHP Version < 7 * * @throws ModuleRequireException */ private function initializeSymfonyCache() { $cache = Configuration::projectDir() . $this->config['var_path'] . DIRECTORY_SEPARATOR . 'bootstrap.php.cache'; if (PHP_VERSION_ID < 70000 && !file_exists($cache)) { throw new ModuleRequireException( __CLASS__, "Symfony bootstrap file not found in $cache\n \n" . "Please specify path to bootstrap file using `var_path` config option\n \n" . "If you are trying to load bootstrap from a Bundle provide path like:\n \n" . "modules:\n enabled:\n" . " - Symfony:\n" . " var_path: '../../app'\n" . " app_path: '../../app'" ); } if (file_exists($cache)) { require_once $cache; } } /** * Initialize new client instance before each test */ public function _before(\Codeception\TestInterface $test) { $this->persistentServices = array_merge($this->persistentServices, $this->permanentServices); $this->client = new SymfonyConnector($this->kernel, $this->persistentServices, $this->config['rebootable_client']); } /** * Update permanent services after each test */ public function _after(\Codeception\TestInterface $test) { foreach ($this->permanentServices as $serviceName => $service) { $this->permanentServices[$serviceName] = $this->grabService($serviceName); } parent::_after($test); } public function onReconfigure($settings = []) { parent::_beforeSuite($settings); $this->_initialize(); } /** * Retrieve Entity Manager. * * EM service is retrieved once and then that instance returned on each call */ public function _getEntityManager() { if ($this->kernel === null) { $this->fail('Symfony2 platform module is not loaded'); } if (!isset($this->permanentServices[$this->config['em_service']])) { // try to persist configured EM $this->persistService($this->config['em_service'], true); if ($this->_getContainer()->has('doctrine')) { $this->persistService('doctrine', true); } if ($this->_getContainer()->has('doctrine.orm.default_entity_manager')) { $this->persistService('doctrine.orm.default_entity_manager', true); } if ($this->_getContainer()->has('doctrine.dbal.backend_connection')) { $this->persistService('doctrine.dbal.backend_connection', true); } } return $this->permanentServices[$this->config['em_service']]; } /** * Return container. * * @return ContainerInterface */ public function _getContainer() { return $this->kernel->getContainer(); } /** * Attempts to guess the kernel location. * * When the Kernel is located, the file is required. * * @return string The Kernel class name */ protected function getKernelClass() { $path = codecept_root_dir() . $this->config['app_path']; if (!file_exists(codecept_root_dir() . $this->config['app_path'])) { throw new ModuleRequireException( __CLASS__, "Can't load Kernel from $path.\n" . "Directory does not exists. Use `app_path` parameter to provide valid application path" ); } $finder = new Finder(); $finder->name('*Kernel.php')->depth('0')->in($path); $results = iterator_to_array($finder); if (!count($results)) { throw new ModuleRequireException( __CLASS__, "File with Kernel class was not found at $path. " . "Specify directory where file with Kernel class for your application is located with `app_path` parameter." ); } if (file_exists(codecept_root_dir() . 'vendor' . DIRECTORY_SEPARATOR . 'autoload.php')) { // ensure autoloader from this dir is loaded require_once codecept_root_dir() . 'vendor' . DIRECTORY_SEPARATOR . 'autoload.php'; } $filesRealPath = array_map(function ($file) { require_once $file; return $file->getRealPath(); }, $results); $possibleKernelClasses = $this->getPossibleKernelClasses(); foreach ($possibleKernelClasses as $class) { if (class_exists($class)) { $refClass = new \ReflectionClass($class); if ($file = array_search($refClass->getFileName(), $filesRealPath)) { return $class; } } } throw new ModuleRequireException( __CLASS__, "Kernel class was not found in $file. " . "Specify directory where file with Kernel class for your application is located with `app_path` parameter." ); } /** * Get service $serviceName and add it to the lists of persistent services. * If $isPermanent then service becomes persistent between tests * * @param string $serviceName * @param boolean $isPermanent */ public function persistService($serviceName, $isPermanent = false) { $service = $this->grabService($serviceName); $this->persistentServices[$serviceName] = $service; if ($isPermanent) { $this->permanentServices[$serviceName] = $service; } if ($this->client) { $this->client->persistentServices[$serviceName] = $service; } } /** * Remove service $serviceName from the lists of persistent services. * * @param string $serviceName */ public function unpersistService($serviceName) { if (isset($this->persistentServices[$serviceName])) { unset($this->persistentServices[$serviceName]); } if (isset($this->permanentServices[$serviceName])) { unset($this->permanentServices[$serviceName]); } if ($this->client && isset($this->client->persistentServices[$serviceName])) { unset($this->client->persistentServices[$serviceName]); } } /** * Invalidate previously cached routes. */ public function invalidateCachedRouter() { $this->unpersistService('router'); } /** * Opens web page using route name and parameters. * * ``` php * <?php * $I->amOnRoute('posts.create'); * $I->amOnRoute('posts.show', array('id' => 34)); * ?> * ``` * * @param $routeName * @param array $params */ public function amOnRoute($routeName, array $params = []) { $router = $this->grabService('router'); if (!$router->getRouteCollection()->get($routeName)) { $this->fail(sprintf('Route with name "%s" does not exists.', $routeName)); } $url = $router->generate($routeName, $params); $this->amOnPage($url); } /** * Checks that current url matches route. * * ``` php * <?php * $I->seeCurrentRouteIs('posts.index'); * $I->seeCurrentRouteIs('posts.show', array('id' => 8)); * ?> * ``` * * @param $routeName * @param array $params */ public function seeCurrentRouteIs($routeName, array $params = []) { $router = $this->grabService('router'); if (!$router->getRouteCollection()->get($routeName)) { $this->fail(sprintf('Route with name "%s" does not exists.', $routeName)); } $uri = explode('?', $this->grabFromCurrentUrl())[0]; try { $match = $router->match($uri); } catch (\Symfony\Component\Routing\Exception\ResourceNotFoundException $e) { $this->fail(sprintf('The "%s" url does not match with any route', $uri)); } $expected = array_merge(['_route' => $routeName], $params); $intersection = array_intersect_assoc($expected, $match); $this->assertEquals($expected, $intersection); } /** * Checks that current url matches route. * Unlike seeCurrentRouteIs, this can matches without exact route parameters * * ``` php * <?php * $I->seeCurrentRouteMatches('my_blog_pages'); * ?> * ``` * * @param $routeName */ public function seeInCurrentRoute($routeName) { $router = $this->grabService('router'); if (!$router->getRouteCollection()->get($routeName)) { $this->fail(sprintf('Route with name "%s" does not exists.', $routeName)); } $uri = explode('?', $this->grabFromCurrentUrl())[0]; try { $matchedRouteName = $router->match($uri)['_route']; } catch (\Symfony\Component\Routing\Exception\ResourceNotFoundException $e) { $this->fail(sprintf('The "%s" url does not match with any route', $uri)); } $this->assertEquals($matchedRouteName, $routeName); } /** * Checks if the desired number of emails was sent. * If no argument is provided then at least one email must be sent to satisfy the check. * The email is checked using Symfony's profiler. If your app performs a redirect after sending the email, * you need to tell Codeception to not follow this redirect, using REST Module's [stopFollowingRedirects]( * https://codeception.com/docs/modules/REST#stopFollowingRedirects) method. * * ``` php * <?php * $I->seeEmailIsSent(2); * ?> * ``` * * @param null|int $expectedCount */ public function seeEmailIsSent($expectedCount = null) { $profile = $this->getProfile(); if (!$profile) { $this->fail('Emails can\'t be tested without Profiler'); } if (!$profile->hasCollector('swiftmailer')) { $this->fail('Emails can\'t be tested without SwiftMailer connector'); } if (!is_int($expectedCount) && !is_null($expectedCount)) { $this->fail(sprintf( 'The required number of emails must be either an integer or null. "%s" was provided.', print_r($expectedCount, true) )); } $realCount = $profile->getCollector('swiftmailer')->getMessageCount(); if ($expectedCount === null) { $this->assertGreaterThan(0, $realCount); } else { $this->assertEquals( $expectedCount, $realCount, sprintf( 'Expected number of sent emails was %d, but in reality %d %s sent.', $expectedCount, $realCount, $realCount === 2 ? 'was' : 'were' ) ); } } /** * Checks that no email was sent. This is an alias for seeEmailIsSent(0). * * @part email */ public function dontSeeEmailIsSent() { $this->seeEmailIsSent(0); } /** * Grabs a service from Symfony DIC container. * Recommended to use for unit testing. * * ``` php * <?php * $em = $I->grabService('doctrine'); * ?> * ``` * * @param $service * @return mixed * @part services */ public function grabService($service) { $container = $this->_getContainer(); if (!$container->has($service)) { $this->fail("Service $service is not available in container"); } return $container->get($service); } /** * @return \Symfony\Component\HttpKernel\Profiler\Profile */ protected function getProfile() { $container = $this->_getContainer(); if (!$container->has('profiler')) { return null; } $profiler = $this->grabService('profiler'); $response = $this->client->getResponse(); if (null === $response) { $this->fail("You must perform a request before using this method."); } return $profiler->loadProfileFromResponse($response); } /** * @param $url */ protected function debugResponse($url) { parent::debugResponse($url); if ($profile = $this->getProfile()) { if ($profile->hasCollector('security')) { if ($profile->getCollector('security')->isAuthenticated()) { $roles = $profile->getCollector('security')->getRoles(); if ($roles instanceof Data) { $roles = $this->extractRawRoles($roles); } $this->debugSection( 'User', $profile->getCollector('security')->getUser() . ' [' . implode(',', $roles) . ']' ); } else { $this->debugSection('User', 'Anonymous'); } } if ($profile->hasCollector('swiftmailer')) { $messages = $profile->getCollector('swiftmailer')->getMessageCount(); if ($messages) { $this->debugSection('Emails', $messages . ' sent'); } } if ($profile->hasCollector('timer')) { $this->debugSection('Time', $profile->getCollector('timer')->getTime()); } } } /** * @param Data $data * @return array */ private function extractRawRoles(Data $data) { if ($this->dataRevealsValue($data)) { $roles = $data->getValue(); } else { $raw = $data->getRawData(); $roles = isset($raw[1]) ? $raw[1] : []; } return $roles; } /** * Returns a list of recognized domain names. * * @return array */ protected function getInternalDomains() { $internalDomains = []; $routes = $this->grabService('router')->getRouteCollection(); /* @var \Symfony\Component\Routing\Route $route */ foreach ($routes as $route) { if (!is_null($route->getHost())) { $compiled = $route->compile(); if (!is_null($compiled->getHostRegex())) { $internalDomains[] = $compiled->getHostRegex(); } } } return array_unique($internalDomains); } /** * Reboot client's kernel. * Can be used to manually reboot kernel when 'rebootable_client' => false * * ``` php * <?php * ... * perform some requests * ... * $I->rebootClientKernel(); * ... * perform other requests * ... * * ?> * ``` * */ public function rebootClientKernel() { if ($this->client) { $this->client->rebootKernel(); } } /** * Public API from Data changed from Symfony 3.2 to 3.3. * * @param \Symfony\Component\VarDumper\Cloner\Data $data * * @return bool */ private function dataRevealsValue(Data $data) { return method_exists($data, 'getValue'); } /** * Returns list of the possible kernel classes based on the module configuration * * @return array */ private function getPossibleKernelClasses() { if (empty($this->config['kernel_class'])) { return self::$possibleKernelClasses; } if (!is_string($this->config['kernel_class'])) { throw new ModuleException( __CLASS__, "Parameter 'kernel_class' must have 'string' type.\n" ); } return [$this->config['kernel_class']]; } } <?php namespace Codeception\Module; use Codeception\Coverage\Subscriber\LocalServer; use Codeception\Exception\ConnectionException; use Codeception\Exception\ElementNotFound; use Codeception\Exception\MalformedLocatorException; use Codeception\Exception\ModuleConfigException as ModuleConfigException; use Codeception\Exception\ModuleException; use Codeception\Exception\TestRuntimeException; use Codeception\Lib\Interfaces\ConflictsWithModule; use Codeception\Lib\Interfaces\ElementLocator; use Codeception\Lib\Interfaces\MultiSession as MultiSessionInterface; use Codeception\Lib\Interfaces\PageSourceSaver; use Codeception\Lib\Interfaces\Remote as RemoteInterface; use Codeception\Lib\Interfaces\RequiresPackage; use Codeception\Lib\Interfaces\ScreenshotSaver; use Codeception\Lib\Interfaces\SessionSnapshot; use Codeception\Lib\Interfaces\Web as WebInterface; use Codeception\Module as CodeceptionModule; use Codeception\PHPUnit\Constraint\Page as PageConstraint; use Codeception\PHPUnit\Constraint\WebDriver as WebDriverConstraint; use Codeception\PHPUnit\Constraint\WebDriverNot as WebDriverConstraintNot; use Codeception\Test\Descriptor; use Codeception\Test\Interfaces\ScenarioDriven; use Codeception\TestInterface; use Codeception\Util\ActionSequence; use Codeception\Util\Debug; use Codeception\Util\Locator; use Codeception\Util\Uri; use Facebook\WebDriver\Cookie; use Facebook\WebDriver\Exception\InvalidElementStateException; use Facebook\WebDriver\Exception\InvalidSelectorException; use Facebook\WebDriver\Exception\NoSuchElementException; use Facebook\WebDriver\Exception\UnknownServerException; use Facebook\WebDriver\Exception\WebDriverCurlException; use Facebook\WebDriver\Interactions\WebDriverActions; use Facebook\WebDriver\Remote\LocalFileDetector; use Facebook\WebDriver\Remote\RemoteWebDriver; use Facebook\WebDriver\Remote\RemoteWebElement; use Facebook\WebDriver\Remote\UselessFileDetector; use Facebook\WebDriver\Remote\WebDriverCapabilityType; use Facebook\WebDriver\WebDriverBy; use Facebook\WebDriver\WebDriverDimension; use Facebook\WebDriver\WebDriverElement; use Facebook\WebDriver\WebDriverExpectedCondition; use Facebook\WebDriver\WebDriverKeys; use Facebook\WebDriver\WebDriverSelect; use GuzzleHttp\Cookie\SetCookie; use Symfony\Component\DomCrawler\Crawler; /** * New generation Selenium WebDriver module. * * ## Local Testing * * ### Selenium * * To run Selenium Server you need [Java](https://www.java.com/) as well as Chrome or Firefox browser installed. * * 1. Download [Selenium Standalone Server](http://docs.seleniumhq.org/download/) * 2. To use Chrome, install [ChromeDriver](https://sites.google.com/a/chromium.org/chromedriver/getting-started). To use Firefox, install [GeckoDriver](https://github.com/mozilla/geckodriver). * 3. Launch the Selenium Server: `java -jar selenium-server-standalone-3.xx.xxx.jar`. To locate Chromedriver binary use `-Dwebdriver.chrome.driver=./chromedriver` option. For Geckodriver use `-Dwebdriver.gecko.driver=./geckodriver`. * 4. Configure this module (in `acceptance.suite.yml`) by setting `url` and `browser`: * * ```yaml * modules: * enabled: * - WebDriver: * url: 'http://localhost/' * browser: chrome # 'chrome' or 'firefox' * ``` * * Launch Selenium Server before executing tests. * * ``` * java -jar "/path/to/selenium-server-standalone-xxx.jar" * ``` * * ### ChromeDriver * * To run tests in Chrome browser you may connect to ChromeDriver directly, without using Selenium Server. * * 1. Install [ChromeDriver](https://sites.google.com/a/chromium.org/chromedriver/getting-started). * 2. Launch ChromeDriver: `chromedriver --url-base=/wd/hub` * 3. Configure this module to use ChromeDriver port: * * ```yaml * modules: * enabled: * - WebDriver: * url: 'http://localhost/' * window_size: false # disabled in ChromeDriver * port: 9515 * browser: chrome * capabilities: * "goog:chromeOptions": # additional chrome options * ``` * * Additional [Chrome options](https://sites.google.com/a/chromium.org/chromedriver/capabilities) can be set in `goog:chromeOptions` capabilities. Note that Selenium 3.8 renamed this capability from `chromeOptions` to `goog:chromeOptions`. * * * ### PhantomJS * * PhantomJS is a [headless browser](https://en.wikipedia.org/wiki/Headless_browser) alternative to Selenium Server that implements * [the WebDriver protocol](https://code.google.com/p/selenium/wiki/JsonWireProtocol). * It allows you to run Selenium tests on a server without a GUI installed. * * 1. Download [PhantomJS](http://phantomjs.org/download.html) * 2. Run PhantomJS in WebDriver mode: `phantomjs --webdriver=4444` * 3. Configure this module (in `acceptance.suite.yml`) by setting url and `phantomjs` as browser: * * ```yaml * modules: * enabled: * - WebDriver: * url: 'http://localhost/' * browser: phantomjs * ``` * * Since PhantomJS doesn't give you any visual feedback, it's probably a good idea to install [Codeception\Extension\Recorder](http://codeception.com/extensions#CodeceptionExtensionRecorder) which gives you screenshots of how PhantomJS "sees" your pages. * * ### Headless Selenium in Docker * * Docker can ship Selenium Server with all its dependencies and browsers inside a single container. * Running tests inside Docker is as easy as pulling [official selenium image](https://github.com/SeleniumHQ/docker-selenium) and starting a container with Chrome: * * ``` * docker run --net=host selenium/standalone-chrome * ``` * * By using `--net=host` we allow selenium to access local websites. * * ## Cloud Testing * * Cloud Testing services can run your WebDriver tests in the cloud. * In case you want to test a local site or site behind a firewall * you should use a tunnel application provided by a service. * * ### SauceLabs * * 1. Create an account at [SauceLabs.com](http://SauceLabs.com) to get your username and access key * 2. In the module configuration use the format `username`:`access_key`@ondemand.saucelabs.com' for `host` * 3. Configure `platform` under `capabilities` to define the [Operating System](https://docs.saucelabs.com/reference/platforms-configurator/#/) * 4. run a tunnel app if your site can't be accessed from Internet * * ```yaml * modules: * enabled: * - WebDriver: * url: http://mysite.com * host: '<username>:<access key>@ondemand.saucelabs.com' * port: 80 * browser: chrome * capabilities: * platform: 'Windows 10' * ``` * * ### BrowserStack * * 1. Create an account at [BrowserStack](https://www.browserstack.com/) to get your username and access key * 2. In the module configuration use the format `username`:`access_key`@hub.browserstack.com' for `host` * 3. Configure `os` and `os_version` under `capabilities` to define the operating System * 4. If your site is available only locally or via VPN you should use a tunnel app. In this case add `browserstack.local` capability and set it to true. * * ```yaml * modules: * enabled: * - WebDriver: * url: http://mysite.com * host: '<username>:<access key>@hub.browserstack.com' * port: 80 * browser: chrome * capabilities: * os: Windows * os_version: 10 * browserstack.local: true # for local testing * ``` * ### TestingBot * * 1. Create an account at [TestingBot](https://testingbot.com/) to get your key and secret * 2. In the module configuration use the format `key`:`secret`@hub.testingbot.com' for `host` * 3. Configure `platform` under `capabilities` to define the [Operating System](https://testingbot.com/support/getting-started/browsers.html) * 4. Run [TestingBot Tunnel](https://testingbot.com/support/other/tunnel) if your site can't be accessed from Internet * * ```yaml * modules: * enabled: * - WebDriver: * url: http://mysite.com * host: '<key>:<secret>@hub.testingbot.com' * port: 80 * browser: chrome * capabilities: * platform: Windows 10 * ``` * * ## Configuration * * * `url` *required* - Starting URL for your app. * * `browser` *required* - Browser to launch. * * `host` - Selenium server host (127.0.0.1 by default). * * `port` - Selenium server port (4444 by default). * * `restart` - Set to `false` (default) to use the same browser window for all tests, or set to `true` to create a new window for each test. In any case, when all tests are finished the browser window is closed. * * `start` - Autostart a browser for tests. Can be disabled if browser session is started with `_initializeSession` inside a Helper. * * `window_size` - Initial window size. Set to `maximize` or a dimension in the format `640x480`. * * `clear_cookies` - Set to false to keep cookies, or set to true (default) to delete all cookies between tests. * * `wait` (default: 0 seconds) - Whenever element is required and is not on page, wait for n seconds to find it before fail. * * `capabilities` - Sets Selenium [desired capabilities](https://github.com/SeleniumHQ/selenium/wiki/DesiredCapabilities). Should be a key-value array. * * `connection_timeout` - timeout for opening a connection to remote selenium server (30 seconds by default). * * `request_timeout` - timeout for a request to return something from remote selenium server (30 seconds by default). * * `pageload_timeout` - amount of time to wait for a page load to complete before throwing an error (default 0 seconds). * * `http_proxy` - sets http proxy server url for testing a remote server. * * `http_proxy_port` - sets http proxy server port * * `debug_log_entries` - how many selenium entries to print with `debugWebDriverLogs` or on fail (15 by default). * * `log_js_errors` - Set to true to include possible JavaScript to HTML report, or set to false (default) to deactivate. * * Example (`acceptance.suite.yml`) * * ```yaml * modules: * enabled: * - WebDriver: * url: 'http://localhost/' * browser: firefox * window_size: 1024x768 * capabilities: * unexpectedAlertBehaviour: 'accept' * firefox_profile: '~/firefox-profiles/codeception-profile.zip.b64' * ``` * * ## Usage * * ### Locating Elements * * Most methods in this module that operate on a DOM element (e.g. `click`) accept a locator as the first argument, * which can be either a string or an array. * * If the locator is an array, it should have a single element, * with the key signifying the locator type (`id`, `name`, `css`, `xpath`, `link`, or `class`) * and the value being the locator itself. * This is called a "strict" locator. * Examples: * * * `['id' => 'foo']` matches `<div id="foo">` * * `['name' => 'foo']` matches `<div name="foo">` * * `['css' => 'input[type=input][value=foo]']` matches `<input type="input" value="foo">` * * `['xpath' => "//input[@type='submit'][contains(@value, 'foo')]"]` matches `<input type="submit" value="foobar">` * * `['link' => 'Click here']` matches `<a target="_top" href="/newspapers?url=google.com">Click here</a>` * * `['class' => 'foo']` matches `<div class="foo">` * * Writing good locators can be tricky. * The Mozilla team has written an excellent guide titled [Writing reliable locators for Selenium and WebDriver tests](https://blog.mozilla.org/webqa/2013/09/26/writing-reliable-locators-for-selenium-and-webdriver-tests/). * * If you prefer, you may also pass a string for the locator. This is called a "fuzzy" locator. * In this case, Codeception uses a a variety of heuristics (depending on the exact method called) to determine what element you're referring to. * For example, here's the heuristic used for the `submitForm` method: * * 1. Does the locator look like an ID selector (e.g. "#foo")? If so, try to find a form matching that ID. * 2. If nothing found, check if locator looks like a CSS selector. If so, run it. * 3. If nothing found, check if locator looks like an XPath expression. If so, run it. * 4. Throw an `ElementNotFound` exception. * * Be warned that fuzzy locators can be significantly slower than strict locators. * Especially if you use Selenium WebDriver with `wait` (aka implicit wait) option. * In the example above if you set `wait` to 5 seconds and use XPath string as fuzzy locator, * `submitForm` method will wait for 5 seconds at each step. * That means 5 seconds finding the form by ID, another 5 seconds finding by CSS * until it finally tries to find the form by XPath). * If speed is a concern, it's recommended you stick with explicitly specifying the locator type via the array syntax. * * ## Public Properties * * * `webDriver` - instance of `\Facebook\WebDriver\Remote\RemoteWebDriver`. Can be accessed from Helper classes for complex WebDriver interactions. * * ```php * // inside Helper class * $this->getModule('WebDriver')->webDriver->getKeyboard()->sendKeys('hello, webdriver'); * ``` * */ class WebDriver extends CodeceptionModule implements WebInterface, RemoteInterface, MultiSessionInterface, SessionSnapshot, ScreenshotSaver, PageSourceSaver, ElementLocator, ConflictsWithModule, RequiresPackage { protected $requiredFields = ['browser', 'url']; protected $config = [ 'protocol' => 'http', 'host' => '127.0.0.1', 'port' => '4444', 'path' => '/wd/hub', 'start' => true, 'restart' => false, 'wait' => 0, 'clear_cookies' => true, 'window_size' => false, 'capabilities' => [], 'connection_timeout' => null, 'request_timeout' => null, 'pageload_timeout' => null, 'http_proxy' => null, 'http_proxy_port' => null, 'ssl_proxy' => null, 'ssl_proxy_port' => null, 'debug_log_entries' => 15, 'log_js_errors' => false ]; protected $wdHost; protected $capabilities; protected $connectionTimeoutInMs; protected $requestTimeoutInMs; protected $test; protected $sessions = []; protected $sessionSnapshots = []; protected $httpProxy; protected $httpProxyPort; protected $sslProxy; protected $sslProxyPort; /** * @var RemoteWebDriver */ public $webDriver; /** * @var RemoteWebElement */ protected $baseElement; public function _requires() { return ['Facebook\WebDriver\Remote\RemoteWebDriver' => '"facebook/webdriver": "^1.0.1"']; } /** * @return RemoteWebElement * @throws ModuleException */ protected function getBaseElement() { if (!$this->baseElement) { throw new ModuleException($this, "Page not loaded. Use `\$I->amOnPage` (or hidden API methods `_request` and `_loadPage`) to open it"); } return $this->baseElement; } public function _initialize() { $this->wdHost = sprintf('%s://%s:%s%s', $this->config['protocol'], $this->config['host'], $this->config['port'], $this->config['path']); $this->capabilities = $this->config['capabilities']; $this->capabilities[WebDriverCapabilityType::BROWSER_NAME] = $this->config['browser']; if ($proxy = $this->getProxy()) { $this->capabilities[WebDriverCapabilityType::PROXY] = $proxy; } $this->connectionTimeoutInMs = $this->config['connection_timeout'] * 1000; $this->requestTimeoutInMs = $this->config['request_timeout'] * 1000; $this->loadFirefoxProfile(); } /** * Change capabilities of WebDriver. Should be executed before starting a new browser session. * This method expects a function to be passed which returns array or [WebDriver Desired Capabilities](https://github.com/facebook/php-webdriver/blob/community/lib/Remote/DesiredCapabilities.php) object. * Additional [Chrome options](https://github.com/facebook/php-webdriver/wiki/ChromeOptions) (like adding extensions) can be passed as well. * * ```php * <?php // in helper * public function _before(TestInterface $test) * { * $this->getModule('WebDriver')->_capabilities(function($currentCapabilities) { * // or new \Facebook\WebDriver\Remote\DesiredCapabilities(); * return \Facebook\WebDriver\Remote\DesiredCapabilities::firefox(); * }); * } * ``` * * to make this work load `\Helper\Acceptance` before `WebDriver` in `acceptance.suite.yml`: * * ```yaml * modules: * enabled: * - \Helper\Acceptance * - WebDriver * ``` * * For instance, [**BrowserStack** cloud service](https://www.browserstack.com/automate/capabilities) may require a test name to be set in capabilities. * This is how it can be done via `_capabilities` method from `Helper\Acceptance`: * * ```php * <?php // inside Helper\Acceptance * public function _before(TestInterface $test) * { * $name = $test->getMetadata()->getName(); * $this->getModule('WebDriver')->_capabilities(function($currentCapabilities) use ($name) { * $currentCapabilities['name'] = $name; * return $currentCapabilities; * }); * } * ``` * In this case, please ensure that `\Helper\Acceptance` is loaded before WebDriver so new capabilities could be applied. * * @api * @param \Closure $capabilityFunction */ public function _capabilities(\Closure $capabilityFunction) { $this->capabilities = $capabilityFunction($this->capabilities); } public function _conflicts() { return 'Codeception\Lib\Interfaces\Web'; } public function _before(TestInterface $test) { if (!isset($this->webDriver) && $this->config['start']) { $this->_initializeSession(); } $this->setBaseElement(); $test->getMetadata()->setCurrent( [ 'browser' => $this->webDriver->getCapabilities()->getBrowserName(), 'capabilities' => $this->webDriver->getCapabilities()->toArray(), ] ); } /** * Restarts a web browser. * Can be used with `_reconfigure` to open browser with different configuration * * ```php * <?php * // inside a Helper * $this->getModule('WebDriver')->_restart(); // just restart * $this->getModule('WebDriver')->_restart(['browser' => $browser]); // reconfigure + restart * ``` * * @param array $config * @api */ public function _restart($config = []) { $this->webDriver->quit(); if (!empty($config)) { $this->_reconfigure($config); } $this->_initializeSession(); } protected function onReconfigure() { $this->_initialize(); } protected function loadFirefoxProfile() { if (!array_key_exists('firefox_profile', $this->config['capabilities'])) { return; } $firefox_profile = $this->config['capabilities']['firefox_profile']; if (file_exists($firefox_profile) === false) { throw new ModuleConfigException( __CLASS__, "Firefox profile does not exist under given path " . $firefox_profile ); } // Set firefox profile as capability $this->capabilities['firefox_profile'] = file_get_contents($firefox_profile); } protected function initialWindowSize() { if ($this->config['window_size'] == 'maximize') { $this->maximizeWindow(); return; } $size = explode('x', $this->config['window_size']); if (count($size) == 2) { $this->resizeWindow(intval($size[0]), intval($size[1])); } } public function _after(TestInterface $test) { if ($this->config['restart']) { $this->stopAllSessions(); return; } if ($this->config['clear_cookies'] && isset($this->webDriver)) { try { $this->webDriver->manage()->deleteAllCookies(); } catch (\Exception $e) { // may cause fatal errors when not handled $this->debug("Error, can't clean cookies after a test: " . $e->getMessage()); } } } public function _failed(TestInterface $test, $fail) { $this->debugWebDriverLogs($test); $filename = preg_replace('~\W~', '.', Descriptor::getTestSignatureUnique($test)); $outputDir = codecept_output_dir(); $this->_saveScreenshot($report = $outputDir . mb_strcut($filename, 0, 245, 'utf-8') . '.fail.png'); $test->getMetadata()->addReport('png', $report); $this->_savePageSource($report = $outputDir . mb_strcut($filename, 0, 244, 'utf-8') . '.fail.html'); $test->getMetadata()->addReport('html', $report); $this->debug("Screenshot and page source were saved into '$outputDir' dir"); } /** * Print out latest Selenium Logs in debug mode * * @param \Codeception\TestInterface $test */ public function debugWebDriverLogs(TestInterface $test = null) { if (!isset($this->webDriver)) { $this->debug('WebDriver::debugWebDriverLogs method has been called when webDriver is not set'); return; } try { // Dump out latest Selenium logs $logs = $this->webDriver->manage()->getAvailableLogTypes(); foreach ($logs as $logType) { $logEntries = array_slice( $this->webDriver->manage()->getLog($logType), -$this->config['debug_log_entries'] ); if (empty($logEntries)) { $this->debugSection("Selenium {$logType} Logs", " EMPTY "); continue; } $this->debugSection("Selenium {$logType} Logs", "\n" . $this->formatLogEntries($logEntries)); if ($logType === 'browser' && $this->config['log_js_errors'] && ($test instanceof ScenarioDriven) ) { $this->logJSErrors($test, $logEntries); } } } catch (\Exception $e) { $this->debug('Unable to retrieve Selenium logs : ' . $e->getMessage()); } } /** * Turns an array of log entries into a human-readable string. * Each log entry is an array with the keys "timestamp", "level", and "message". * See https://code.google.com/p/selenium/wiki/JsonWireProtocol#Log_Entry_JSON_Object * * @param array $logEntries * @return string */ protected function formatLogEntries(array $logEntries) { $formattedLogs = ''; foreach ($logEntries as $logEntry) { // Timestamp is in milliseconds, but date() requires seconds. $time = date('H:i:s', $logEntry['timestamp'] / 1000) . // Append the milliseconds to the end of the time string '.' . ($logEntry['timestamp'] % 1000); $formattedLogs .= "{$time} {$logEntry['level']} - {$logEntry['message']}\n"; } return $formattedLogs; } /** * Logs JavaScript errors as comments. * * @param ScenarioDriven $test * @param array $browserLogEntries */ protected function logJSErrors(ScenarioDriven $test, array $browserLogEntries) { foreach ($browserLogEntries as $logEntry) { if (true === isset($logEntry['level']) && true === isset($logEntry['message']) && $this->isJSError($logEntry['level'], $logEntry['message']) ) { // Timestamp is in milliseconds, but date() requires seconds. $time = date('H:i:s', $logEntry['timestamp'] / 1000) . // Append the milliseconds to the end of the time string '.' . ($logEntry['timestamp'] % 1000); $test->getScenario()->comment("{$time} {$logEntry['level']} - {$logEntry['message']}"); } } } /** * Determines if the log entry is an error. * The decision is made depending on browser and log-level. * * @param string $logEntryLevel * @param string $message * @return bool */ protected function isJSError($logEntryLevel, $message) { return ( ($this->isPhantom() && $logEntryLevel != 'INFO') // phantomjs logs errors as "WARNING" || $logEntryLevel === 'SEVERE' // other browsers log errors as "SEVERE" ) && strpos($message, 'ERR_PROXY_CONNECTION_FAILED') === false; // ignore blackhole proxy } public function _afterSuite() { // this is just to make sure webDriver is cleared after suite $this->stopAllSessions(); } protected function stopAllSessions() { foreach ($this->sessions as $session) { $this->_closeSession($session); } $this->webDriver = null; $this->baseElement = null; } public function amOnSubdomain($subdomain) { $url = $this->config['url']; $url = preg_replace('~(https?:\/\/)(.*\.)(.*\.)~', "$1$3", $url); // removing current subdomain $url = preg_replace('~(https?:\/\/)(.*)~', "$1$subdomain.$2", $url); // inserting new $this->_reconfigure(['url' => $url]); } /** * Returns URL of a host. * * @api * @return mixed * @throws ModuleConfigException */ public function _getUrl() { if (!isset($this->config['url'])) { throw new ModuleConfigException( __CLASS__, "Module connection failure. The URL for client can't bre retrieved" ); } return $this->config['url']; } protected function getProxy() { $proxyConfig = []; if ($this->config['http_proxy']) { $proxyConfig['httpProxy'] = $this->config['http_proxy']; if ($this->config['http_proxy_port']) { $proxyConfig['httpProxy'] .= ':' . $this->config['http_proxy_port']; } } if ($this->config['ssl_proxy']) { $proxyConfig['sslProxy'] = $this->config['ssl_proxy']; if ($this->config['ssl_proxy_port']) { $proxyConfig['sslProxy'] .= ':' . $this->config['ssl_proxy_port']; } } if (!empty($proxyConfig)) { $proxyConfig['proxyType'] = 'manual'; return $proxyConfig; } return null; } /** * Uri of currently opened page. * @return string * @api * @throws ModuleException */ public function _getCurrentUri() { $url = $this->webDriver->getCurrentURL(); if ($url == 'about:blank' || strpos($url, 'data:') === 0) { throw new ModuleException($this, 'Current url is blank, no page was opened'); } return Uri::retrieveUri($url); } public function _saveScreenshot($filename) { if (!isset($this->webDriver)) { $this->debug('WebDriver::_saveScreenshot method has been called when webDriver is not set'); return; } try { $this->webDriver->takeScreenshot($filename); } catch (\Exception $e) { $this->debug('Unable to retrieve screenshot from Selenium : ' . $e->getMessage()); return; } } public function _findElements($locator) { return $this->match($this->webDriver, $locator); } /** * Saves HTML source of a page to a file * @param $filename */ public function _savePageSource($filename) { if (!isset($this->webDriver)) { $this->debug('WebDriver::_savePageSource method has been called when webDriver is not set'); return; } try { file_put_contents($filename, $this->webDriver->getPageSource()); } catch (\Exception $e) { $this->debug('Unable to retrieve source page from Selenium : ' . $e->getMessage()); } } /** * Takes a screenshot of the current window and saves it to `tests/_output/debug`. * * ``` php * <?php * $I->amOnPage('/user/edit'); * $I->makeScreenshot('edit_page'); * // saved to: tests/_output/debug/edit_page.png * $I->makeScreenshot(); * // saved to: tests/_output/debug/2017-05-26_14-24-11_4b3403665fea6.png * ``` * * @param $name */ public function makeScreenshot($name = null) { if (empty($name)) { $name = uniqid(date("Y-m-d_H-i-s_")); } $debugDir = codecept_log_dir() . 'debug'; if (!is_dir($debugDir)) { mkdir($debugDir, 0777); } $screenName = $debugDir . DIRECTORY_SEPARATOR . $name . '.png'; $this->_saveScreenshot($screenName); $this->debugSection('Screenshot Saved', "file://$screenName"); } public function makeHtmlSnapshot($name = null) { if (empty($name)) { $name = uniqid(date("Y-m-d_H-i-s_")); } $debugDir = codecept_output_dir() . 'debug'; if (!is_dir($debugDir)) { mkdir($debugDir, 0777); } $fileName = $debugDir . DIRECTORY_SEPARATOR . $name . '.html'; $this->_savePageSource($fileName); $this->debugSection('Snapshot Saved', "file://$fileName"); } /** * Resize the current window. * * ``` php * <?php * $I->resizeWindow(800, 600); * * ``` * * @param int $width * @param int $height */ public function resizeWindow($width, $height) { $this->webDriver->manage()->window()->setSize(new WebDriverDimension($width, $height)); } public function seeCookie($cookie, array $params = []) { $cookies = $this->filterCookies($this->webDriver->manage()->getCookies(), $params); $cookies = array_map( function ($c) { return $c['name']; }, $cookies ); $this->debugSection('Cookies', json_encode($this->webDriver->manage()->getCookies())); $this->assertContains($cookie, $cookies); } public function dontSeeCookie($cookie, array $params = []) { $cookies = $this->filterCookies($this->webDriver->manage()->getCookies(), $params); $cookies = array_map( function ($c) { return $c['name']; }, $cookies ); $this->debugSection('Cookies', json_encode($this->webDriver->manage()->getCookies())); $this->assertNotContains($cookie, $cookies); } public function setCookie($cookie, $value, array $params = []) { $params['name'] = $cookie; $params['value'] = $value; if (isset($params['expires'])) { // PhpBrowser compatibility $params['expiry'] = $params['expires']; } if (!isset($params['domain'])) { $urlParts = parse_url($this->config['url']); if (isset($urlParts['host'])) { $params['domain'] = $urlParts['host']; } } // #5401 Supply defaults, otherwise chromedriver 2.46 complains. $defaults = [ 'path' => '/', 'expiry' => time() + 86400, 'secure' => false, 'httpOnly' => false, ]; foreach ($defaults as $key => $default) { if (empty($params[$key])) { $params[$key] = $default; } } $this->webDriver->manage()->addCookie($params); $this->debugSection('Cookies', json_encode($this->webDriver->manage()->getCookies())); } public function resetCookie($cookie, array $params = []) { $this->webDriver->manage()->deleteCookieNamed($cookie); $this->debugSection('Cookies', json_encode($this->webDriver->manage()->getCookies())); } public function grabCookie($cookie, array $params = []) { $params['name'] = $cookie; $cookies = $this->filterCookies($this->webDriver->manage()->getCookies(), $params); if (empty($cookies)) { return null; } $cookie = reset($cookies); return $cookie['value']; } /** * Grabs current page source code. * * @throws ModuleException if no page was opened. * * @return string Current page source code. */ public function grabPageSource() { // Make sure that some page was opened. $this->_getCurrentUri(); return $this->webDriver->getPageSource(); } protected function filterCookies($cookies, $params = []) { foreach (['domain', 'path', 'name'] as $filter) { if (!isset($params[$filter])) { continue; } $cookies = array_filter( $cookies, function ($item) use ($filter, $params) { return $item[$filter] == $params[$filter]; } ); } return $cookies; } public function amOnUrl($url) { $host = Uri::retrieveHost($url); $this->_reconfigure(['url' => $host]); $this->debugSection('Host', $host); $this->webDriver->get($url); } public function amOnPage($page) { $url = Uri::appendPath($this->config['url'], $page); $this->debugSection('GET', $url); $this->webDriver->get($url); } public function see($text, $selector = null) { if (!$selector) { return $this->assertPageContains($text); } $this->enableImplicitWait(); $nodes = $this->matchVisible($selector); $this->disableImplicitWait(); $this->assertNodesContain($text, $nodes, $selector); } public function dontSee($text, $selector = null) { if (!$selector) { return $this->assertPageNotContains($text); } $nodes = $this->matchVisible($selector); $this->assertNodesNotContain($text, $nodes, $selector); } public function seeInSource($raw) { $this->assertPageSourceContains($raw); } public function dontSeeInSource($raw) { $this->assertPageSourceNotContains($raw); } /** * Checks that the page source contains the given string. * * ```php * <?php * $I->seeInPageSource('<link rel="apple-touch-icon"'); * ``` * * @param $text */ public function seeInPageSource($text) { $this->assertThat( $this->webDriver->getPageSource(), new PageConstraint($text, $this->_getCurrentUri()), '' ); } /** * Checks that the page source doesn't contain the given string. * * @param $text */ public function dontSeeInPageSource($text) { $this->assertThatItsNot( $this->webDriver->getPageSource(), new PageConstraint($text, $this->_getCurrentUri()), '' ); } public function click($link, $context = null) { $page = $this->webDriver; if ($context) { $page = $this->matchFirstOrFail($this->webDriver, $context); } $el = $this->_findClickable($page, $link); if (!$el) { // check one more time if this was a CSS selector we didn't match try { $els = $this->match($page, $link); } catch (MalformedLocatorException $e) { throw new ElementNotFound("name=$link", "'$link' is invalid CSS and XPath selector and Link or Button"); } $el = reset($els); } if (!$el) { throw new ElementNotFound($link, 'Link or Button or CSS or XPath'); } $el->click(); } /** * Locates a clickable element. * * Use it in Helpers or GroupObject or Extension classes: * * ```php * <?php * $module = $this->getModule('WebDriver'); * $page = $module->webDriver; * * // search a link or button on a page * $el = $module->_findClickable($page, 'Click Me'); * * // search a link or button within an element * $topBar = $module->_findElements('.top-bar')[0]; * $el = $module->_findClickable($topBar, 'Click Me'); * * ``` * @api * @param RemoteWebDriver $page WebDriver instance or an element to search within * @param $link a link text or locator to click * @return WebDriverElement */ public function _findClickable($page, $link) { if (is_array($link) or ($link instanceof WebDriverBy)) { return $this->matchFirstOrFail($page, $link); } // try to match by strict locators, CSS Ids or XPath if (Locator::isPrecise($link)) { return $this->matchFirstOrFail($page, $link); } $locator = Crawler::xpathLiteral(trim($link)); // narrow $xpath = Locator::combine( ".//a[normalize-space(.)=$locator]", ".//button[normalize-space(.)=$locator]", ".//a/img[normalize-space(@alt)=$locator]/ancestor::a", ".//input[./@type = 'submit' or ./@type = 'image' or ./@type = 'button'][normalize-space(@value)=$locator]" ); $els = $page->findElements(WebDriverBy::xpath($xpath)); if (count($els)) { return reset($els); } // wide $xpath = Locator::combine( ".//a[./@href][((contains(normalize-space(string(.)), $locator)) or contains(./@title, $locator) or .//img[contains(./@alt, $locator)])]", ".//input[./@type = 'submit' or ./@type = 'image' or ./@type = 'button'][contains(./@value, $locator)]", ".//input[./@type = 'image'][contains(./@alt, $locator)]", ".//button[contains(normalize-space(string(.)), $locator)]", ".//input[./@type = 'submit' or ./@type = 'image' or ./@type = 'button'][./@name = $locator or ./@title = $locator]", ".//button[./@name = $locator or ./@title = $locator]" ); $els = $page->findElements(WebDriverBy::xpath($xpath)); if (count($els)) { return reset($els); } return null; } /** * @param $selector * @return WebDriverElement[] * @throws \Codeception\Exception\ElementNotFound */ protected function findFields($selector) { if ($selector instanceof WebDriverElement) { return [$selector]; } if (is_array($selector) || ($selector instanceof WebDriverBy)) { $fields = $this->match($this->webDriver, $selector); if (empty($fields)) { throw new ElementNotFound($selector); } return $fields; } $locator = Crawler::xpathLiteral(trim($selector)); // by text or label $xpath = Locator::combine( // @codingStandardsIgnoreStart ".//*[self::input | self::textarea | self::select][not(./@type = 'submit' or ./@type = 'image' or ./@type = 'hidden')][(((./@name = $locator) or ./@id = //label[contains(normalize-space(string(.)), $locator)]/@for) or ./@placeholder = $locator)]", ".//label[contains(normalize-space(string(.)), $locator)]//.//*[self::input | self::textarea | self::select][not(./@type = 'submit' or ./@type = 'image' or ./@type = 'hidden')]" // @codingStandardsIgnoreEnd ); $fields = $this->webDriver->findElements(WebDriverBy::xpath($xpath)); if (!empty($fields)) { return $fields; } // by name $xpath = ".//*[self::input | self::textarea | self::select][@name = $locator]"; $fields = $this->webDriver->findElements(WebDriverBy::xpath($xpath)); if (!empty($fields)) { return $fields; } // try to match by CSS or XPath $fields = $this->match($this->webDriver, $selector, false); if (!empty($fields)) { return $fields; } throw new ElementNotFound($selector, "Field by name, label, CSS or XPath"); } /** * @param $selector * @return WebDriverElement * @throws \Codeception\Exception\ElementNotFound */ protected function findField($selector) { $arr = $this->findFields($selector); return reset($arr); } public function seeLink($text, $url = null) { $this->enableImplicitWait(); $nodes = $this->getBaseElement()->findElements(WebDriverBy::partialLinkText($text)); $this->disableImplicitWait(); $currentUri = $this->_getCurrentUri(); if (empty($nodes)) { $this->fail("No links containing text '$text' were found in page $currentUri"); } if ($url) { $nodes = $this->filterNodesByHref($url, $nodes); } $this->assertNotEmpty($nodes, "No links containing text '$text' and URL '$url' were found in page $currentUri"); } public function dontSeeLink($text, $url = null) { $nodes = $this->getBaseElement()->findElements(WebDriverBy::partialLinkText($text)); $currentUri = $this->_getCurrentUri(); if (!$url) { $this->assertEmpty($nodes, "Link containing text '$text' was found in page $currentUri"); } else { $nodes = $this->filterNodesByHref($url, $nodes); $this->assertEmpty($nodes, "Link containing text '$text' and URL '$url' was found in page $currentUri"); } } /** * @param string $url * @param $nodes * @return array */ private function filterNodesByHref($url, $nodes) { //current uri can be relative, merging it with configured base url gives absolute url $absoluteCurrentUrl = Uri::mergeUrls($this->_getUrl(), $this->_getCurrentUri()); $expectedUrl = Uri::mergeUrls($absoluteCurrentUrl, $url); $nodes = array_filter( $nodes, function (WebDriverElement $e) use ($expectedUrl, $absoluteCurrentUrl) { $elementHref = Uri::mergeUrls($absoluteCurrentUrl, $e->getAttribute('href')); return $elementHref === $expectedUrl; } ); return $nodes; } public function seeInCurrentUrl($uri) { $this->assertStringContainsString($uri, $this->_getCurrentUri()); } public function seeCurrentUrlEquals($uri) { $this->assertEquals($uri, $this->_getCurrentUri()); } public function seeCurrentUrlMatches($uri) { $this->assertRegExp($uri, $this->_getCurrentUri()); } public function dontSeeInCurrentUrl($uri) { $this->assertStringNotContainsString($uri, $this->_getCurrentUri()); } public function dontSeeCurrentUrlEquals($uri) { $this->assertNotEquals($uri, $this->_getCurrentUri()); } public function dontSeeCurrentUrlMatches($uri) { $this->assertNotRegExp($uri, $this->_getCurrentUri()); } public function grabFromCurrentUrl($uri = null) { if (!$uri) { return $this->_getCurrentUri(); } $matches = []; $res = preg_match($uri, $this->_getCurrentUri(), $matches); if (!$res) { $this->fail("Couldn't match $uri in " . $this->_getCurrentUri()); } if (!isset($matches[1])) { $this->fail("Nothing to grab. A regex parameter required. Ex: '/user/(\\d+)'"); } return $matches[1]; } public function seeCheckboxIsChecked($checkbox) { $this->assertTrue($this->findField($checkbox)->isSelected()); } public function dontSeeCheckboxIsChecked($checkbox) { $this->assertFalse($this->findField($checkbox)->isSelected()); } public function seeInField($field, $value) { $els = $this->findFields($field); $this->assert($this->proceedSeeInField($els, $value)); } public function dontSeeInField($field, $value) { $els = $this->findFields($field); $this->assertNot($this->proceedSeeInField($els, $value)); } public function seeInFormFields($formSelector, array $params) { $this->proceedSeeInFormFields($formSelector, $params, false); } public function dontSeeInFormFields($formSelector, array $params) { $this->proceedSeeInFormFields($formSelector, $params, true); } protected function proceedSeeInFormFields($formSelector, array $params, $assertNot) { $form = $this->match($this->getBaseElement(), $formSelector); if (empty($form)) { throw new ElementNotFound($formSelector, "Form via CSS or XPath"); } $form = reset($form); $els = []; foreach ($params as $name => $values) { $this->pushFormField($els, $form, $name, $values); } foreach ($els as $arrayElement) { list($el, $values) = $arrayElement; if (!is_array($values)) { $values = [$values]; } foreach ($values as $value) { $ret = $this->proceedSeeInField($el, $value); if ($assertNot) { $this->assertNot($ret); } else { $this->assert($ret); } } } } /** * Map an array element passed to seeInFormFields to its corresponding WebDriver element, * recursing through array values if the field is not found. * * @param array $els The previously found elements. * @param RemoteWebElement $form The form in which to search for fields. * @param string $name The field's name. * @param mixed $values * @return void */ protected function pushFormField(&$els, $form, $name, $values) { $el = $form->findElements(WebDriverBy::name($name)); if ($el) { $els[] = [$el, $values]; } elseif (is_array($values)) { foreach ($values as $key => $value) { $this->pushFormField($els, $form, "{$name}[$key]", $value); } } else { throw new ElementNotFound($name); } } /** * @param RemoteWebElement[] $elements * @param $value * @return array */ protected function proceedSeeInField(array $elements, $value) { $strField = reset($elements)->getAttribute('name'); if (reset($elements)->getTagName() === 'select') { $el = reset($elements); $elements = $el->findElements(WebDriverBy::xpath('.//option')); if (empty($value) && empty($elements)) { return ['True', true]; } } $currentValues = []; if (is_bool($value)) { $currentValues = [false]; } foreach ($elements as $el) { switch ($el->getTagName()) { case 'input': if ($el->getAttribute('type') === 'radio' || $el->getAttribute('type') === 'checkbox') { if ($el->getAttribute('checked')) { if (is_bool($value)) { $currentValues = [true]; break; } else { $currentValues[] = $el->getAttribute('value'); } } } else { $currentValues[] = $el->getAttribute('value'); } break; case 'option': // no break we need the trim text and the value also if (!$el->isSelected()) { break; } $currentValues[] = $el->getText(); case 'textarea': // we include trimmed and real value of textarea for check $currentValues[] = trim($el->getText()); default: $currentValues[] = $el->getAttribute('value'); // raw value break; } } return [ 'Contains', $value, $currentValues, "Failed testing for '$value' in $strField's value: '" . implode("', '", $currentValues) . "'" ]; } public function selectOption($select, $option) { $el = $this->findField($select); if ($el->getTagName() != 'select') { $els = $this->matchCheckables($select); $radio = null; foreach ($els as $el) { $radio = $this->findCheckable($el, $option, true); if ($radio) { break; } } if (!$radio) { throw new ElementNotFound($select, "Radiobutton with value or name '$option in"); } $radio->click(); return; } $wdSelect = new WebDriverSelect($el); if ($wdSelect->isMultiple()) { $wdSelect->deselectAll(); } if (!is_array($option)) { $option = [$option]; } $matched = false; if (key($option) !== 'value') { foreach ($option as $opt) { try { $wdSelect->selectByVisibleText($opt); $matched = true; } catch (NoSuchElementException $e) { } } } if ($matched) { return; } if (key($option) !== 'text') { foreach ($option as $opt) { try { $wdSelect->selectByValue($opt); $matched = true; } catch (NoSuchElementException $e) { } } } if ($matched) { return; } // partially matching foreach ($option as $opt) { try { $optElement = $el->findElement(WebDriverBy::xpath('.//option [contains (., "' . $opt . '")]')); $matched = true; if (!$optElement->isSelected()) { $optElement->click(); } } catch (NoSuchElementException $e) { // exception treated at the end } } if ($matched) { return; } throw new ElementNotFound(json_encode($option), "Option inside $select matched by name or value"); } /** * Manually starts a new browser session. * * ```php * <?php * $this->getModule('WebDriver')->_initializeSession(); * ``` * * @api */ public function _initializeSession() { try { $this->sessions[] = $this->webDriver; $this->webDriver = RemoteWebDriver::create( $this->wdHost, $this->capabilities, $this->connectionTimeoutInMs, $this->requestTimeoutInMs, $this->httpProxy, $this->httpProxyPort ); if (!is_null($this->config['pageload_timeout'])) { $this->webDriver->manage()->timeouts()->pageLoadTimeout($this->config['pageload_timeout']); } $this->setBaseElement(); $this->initialWindowSize(); } catch (WebDriverCurlException $e) { codecept_debug('Curl error: ' . $e->getMessage()); throw new ConnectionException("Can't connect to Webdriver at {$this->wdHost}. Please make sure that Selenium Server or PhantomJS is running."); } } /** * Loads current RemoteWebDriver instance as a session * * @api * @param RemoteWebDriver $session */ public function _loadSession($session) { $this->webDriver = $session; $this->setBaseElement(); } /** * Returns current WebDriver session for saving * * @api * @return RemoteWebDriver */ public function _backupSession() { return $this->webDriver; } /** * Manually closes current WebDriver session. * * ```php * <?php * $this->getModule('WebDriver')->_closeSession(); * * // close a specific session * $webDriver = $this->getModule('WebDriver')->webDriver; * $this->getModule('WebDriver')->_closeSession($webDriver); * ``` * * @api * @param $webDriver (optional) a specific webdriver session instance */ public function _closeSession($webDriver = null) { if (!$webDriver and $this->webDriver) { $webDriver = $this->webDriver; } if (!$webDriver) { return; } try { $webDriver->quit(); unset($webDriver); } catch (UnknownServerException $e) { // Session already closed so nothing to do } } /** * Unselect an option in the given select box. * * @param $select * @param $option */ public function unselectOption($select, $option) { $el = $this->findField($select); $wdSelect = new WebDriverSelect($el); if (!is_array($option)) { $option = [$option]; } $matched = false; foreach ($option as $opt) { try { $wdSelect->deselectByVisibleText($opt); $matched = true; } catch (NoSuchElementException $e) { // exception treated at the end } try { $wdSelect->deselectByValue($opt); $matched = true; } catch (NoSuchElementException $e) { // exception treated at the end } } if ($matched) { return; } throw new ElementNotFound(json_encode($option), "Option inside $select matched by name or value"); } /** * @param $context * @param $radioOrCheckbox * @param bool $byValue * @return mixed|null */ protected function findCheckable($context, $radioOrCheckbox, $byValue = false) { if ($radioOrCheckbox instanceof WebDriverElement) { return $radioOrCheckbox; } if (is_array($radioOrCheckbox) or ($radioOrCheckbox instanceof WebDriverBy)) { return $this->matchFirstOrFail($this->getBaseElement(), $radioOrCheckbox); } $locator = Crawler::xpathLiteral($radioOrCheckbox); if ($context instanceof WebDriverElement && $context->getTagName() === 'input') { $contextType = $context->getAttribute('type'); if (!in_array($contextType, ['checkbox', 'radio'], true)) { return null; } $nameLiteral = Crawler::xPathLiteral($context->getAttribute('name')); $typeLiteral = Crawler::xPathLiteral($contextType); $inputLocatorFragment = "input[@type = $typeLiteral][@name = $nameLiteral]"; $xpath = Locator::combine( // @codingStandardsIgnoreStart "ancestor::form//{$inputLocatorFragment}[(@id = ancestor::form//label[contains(normalize-space(string(.)), $locator)]/@for) or @placeholder = $locator]", // @codingStandardsIgnoreEnd "ancestor::form//label[contains(normalize-space(string(.)), $locator)]//{$inputLocatorFragment}" ); if ($byValue) { $xpath = Locator::combine($xpath, "ancestor::form//{$inputLocatorFragment}[@value = $locator]"); } } else { $xpath = Locator::combine( // @codingStandardsIgnoreStart "//input[@type = 'checkbox' or @type = 'radio'][(@id = //label[contains(normalize-space(string(.)), $locator)]/@for) or @placeholder = $locator or @name = $locator]", // @codingStandardsIgnoreEnd "//label[contains(normalize-space(string(.)), $locator)]//input[@type = 'radio' or @type = 'checkbox']" ); if ($byValue) { $xpath = Locator::combine($xpath, "//input[@type = 'checkbox' or @type = 'radio'][@value = $locator]"); } } $els = $context->findElements(WebDriverBy::xpath($xpath)); if (count($els)) { return reset($els); } $els = $context->findElements(WebDriverBy::xpath(str_replace('ancestor::form', '', $xpath))); if (count($els)) { return reset($els); } $els = $this->match($context, $radioOrCheckbox); if (count($els)) { return reset($els); } return null; } protected function matchCheckables($selector) { $els = $this->match($this->webDriver, $selector); if (!count($els)) { throw new ElementNotFound($selector, "Element containing radio by CSS or XPath"); } return $els; } public function checkOption($option) { $field = $this->findCheckable($this->webDriver, $option); if (!$field) { throw new ElementNotFound($option, "Checkbox or Radio by Label or CSS or XPath"); } if ($field->isSelected()) { return; } $field->click(); } public function uncheckOption($option) { $field = $this->findCheckable($this->getBaseElement(), $option); if (!$field) { throw new ElementNotFound($option, "Checkbox by Label or CSS or XPath"); } if (!$field->isSelected()) { return; } $field->click(); } public function fillField($field, $value) { $el = $this->findField($field); $el->clear(); $el->sendKeys((string)$value); } /** * Clears given field which isn't empty. * * ``` php * <?php * $I->clearField('#username'); * ``` * * @param $field */ public function clearField($field) { $el = $this->findField($field); $el->clear(); } public function attachFile($field, $filename) { $el = $this->findField($field); // in order to be compatible on different OS $filePath = codecept_data_dir() . $filename; if (!file_exists($filePath)) { throw new \InvalidArgumentException("File does not exist: $filePath"); } if (!is_readable($filePath)) { throw new \InvalidArgumentException("File is not readable: $filePath"); } // in order for remote upload to be enabled $el->setFileDetector(new LocalFileDetector()); // skip file detector for phantomjs if ($this->isPhantom()) { $el->setFileDetector(new UselessFileDetector()); } $el->sendKeys(realpath($filePath)); } /** * Grabs all visible text from the current page. * * @return string */ protected function getVisibleText() { if ($this->getBaseElement() instanceof RemoteWebElement) { return $this->getBaseElement()->getText(); } $els = $this->getBaseElement()->findElements(WebDriverBy::cssSelector('body')); if (isset($els[0])) { return $els[0]->getText(); } return ''; } public function grabTextFrom($cssOrXPathOrRegex) { $els = $this->match($this->getBaseElement(), $cssOrXPathOrRegex, false); if (count($els)) { return $els[0]->getText(); } if (@preg_match($cssOrXPathOrRegex, $this->webDriver->getPageSource(), $matches)) { return $matches[1]; } throw new ElementNotFound($cssOrXPathOrRegex, 'CSS or XPath or Regex'); } public function grabAttributeFrom($cssOrXpath, $attribute) { $el = $this->matchFirstOrFail($this->getBaseElement(), $cssOrXpath); return $el->getAttribute($attribute); } public function grabValueFrom($field) { $el = $this->findField($field); // value of multiple select is the value of the first selected option if ($el->getTagName() == 'select') { $select = new WebDriverSelect($el); return $select->getFirstSelectedOption()->getAttribute('value'); } return $el->getAttribute('value'); } public function grabMultiple($cssOrXpath, $attribute = null) { $els = $this->match($this->getBaseElement(), $cssOrXpath); return array_map( function (WebDriverElement $e) use ($attribute) { if ($attribute) { return $e->getAttribute($attribute); } return $e->getText(); }, $els ); } protected function filterByAttributes($els, array $attributes) { foreach ($attributes as $attr => $value) { $els = array_filter( $els, function (WebDriverElement $el) use ($attr, $value) { return $el->getAttribute($attr) == $value; } ); } return $els; } public function seeElement($selector, $attributes = []) { $this->enableImplicitWait(); $els = $this->matchVisible($selector); $this->disableImplicitWait(); $els = $this->filterByAttributes($els, $attributes); $this->assertNotEmpty($els); } public function dontSeeElement($selector, $attributes = []) { $els = $this->matchVisible($selector); $els = $this->filterByAttributes($els, $attributes); $this->assertEmpty($els); } /** * Checks that the given element exists on the page, even it is invisible. * * ``` php * <?php * $I->seeElementInDOM('//form/input[type=hidden]'); * ?> * ``` * * @param $selector * @param array $attributes */ public function seeElementInDOM($selector, $attributes = []) { $this->enableImplicitWait(); $els = $this->match($this->getBaseElement(), $selector); $els = $this->filterByAttributes($els, $attributes); $this->disableImplicitWait(); $this->assertNotEmpty($els); } /** * Opposite of `seeElementInDOM`. * * @param $selector * @param array $attributes */ public function dontSeeElementInDOM($selector, $attributes = []) { $els = $this->match($this->getBaseElement(), $selector); $els = $this->filterByAttributes($els, $attributes); $this->assertEmpty($els); } public function seeNumberOfElements($selector, $expected) { $counted = count($this->matchVisible($selector)); if (is_array($expected)) { list($floor, $ceil) = $expected; $this->assertTrue( $floor <= $counted && $ceil >= $counted, 'Number of elements counted differs from expected range' ); } else { $this->assertEquals( $expected, $counted, 'Number of elements counted differs from expected number' ); } } public function seeNumberOfElementsInDOM($selector, $expected) { $counted = count($this->match($this->getBaseElement(), $selector)); if (is_array($expected)) { list($floor, $ceil) = $expected; $this->assertTrue( $floor <= $counted && $ceil >= $counted, 'Number of elements counted differs from expected range' ); } else { $this->assertEquals( $expected, $counted, 'Number of elements counted differs from expected number' ); } } public function seeOptionIsSelected($selector, $optionText) { $el = $this->findField($selector); if ($el->getTagName() !== 'select') { $els = $this->matchCheckables($selector); foreach ($els as $k => $el) { $els[$k] = $this->findCheckable($el, $optionText, true); } $this->assertNotEmpty( array_filter( $els, function ($e) { return $e && $e->isSelected(); } ) ); return; } $select = new WebDriverSelect($el); $this->assertNodesContain($optionText, $select->getAllSelectedOptions(), 'option'); } public function dontSeeOptionIsSelected($selector, $optionText) { $el = $this->findField($selector); if ($el->getTagName() !== 'select') { $els = $this->matchCheckables($selector); foreach ($els as $k => $el) { $els[$k] = $this->findCheckable($el, $optionText, true); } $this->assertEmpty( array_filter( $els, function ($e) { return $e && $e->isSelected(); } ) ); return; } $select = new WebDriverSelect($el); $this->assertNodesNotContain($optionText, $select->getAllSelectedOptions(), 'option'); } public function seeInTitle($title) { $this->assertStringContainsString($title, $this->webDriver->getTitle()); } public function dontSeeInTitle($title) { $this->assertStringNotContainsString($title, $this->webDriver->getTitle()); } /** * Accepts the active JavaScript native popup window, as created by `window.alert`|`window.confirm`|`window.prompt`. * Don't confuse popups with modal windows, * as created by [various libraries](http://jster.net/category/windows-modals-popups). */ public function acceptPopup() { if ($this->isPhantom()) { throw new ModuleException($this, 'PhantomJS does not support working with popups'); } $this->webDriver->switchTo()->alert()->accept(); } /** * Dismisses the active JavaScript popup, as created by `window.alert`, `window.confirm`, or `window.prompt`. */ public function cancelPopup() { if ($this->isPhantom()) { throw new ModuleException($this, 'PhantomJS does not support working with popups'); } $this->webDriver->switchTo()->alert()->dismiss(); } /** * Checks that the active JavaScript popup, * as created by `window.alert`|`window.confirm`|`window.prompt`, contains the given string. * * @param $text * * @throws \Codeception\Exception\ModuleException */ public function seeInPopup($text) { if ($this->isPhantom()) { throw new ModuleException($this, 'PhantomJS does not support working with popups'); } $alert = $this->webDriver->switchTo()->alert(); try { $this->assertStringContainsString($text, $alert->getText()); } catch (\PHPUnit\Framework\AssertionFailedError $e) { $alert->dismiss(); throw $e; } } /** * Checks that the active JavaScript popup, * as created by `window.alert`|`window.confirm`|`window.prompt`, does NOT contain the given string. * * @param $text * * @throws \Codeception\Exception\ModuleException */ public function dontSeeInPopup($text) { if ($this->isPhantom()) { throw new ModuleException($this, 'PhantomJS does not support working with popups'); } $alert = $this->webDriver->switchTo()->alert(); try { $this->assertStringNotContainsString($text, $alert->getText()); } catch (\PHPUnit\Framework\AssertionFailedError $e) { $alert->dismiss(); throw $e; } } /** * Enters text into a native JavaScript prompt popup, as created by `window.prompt`. * * @param $keys * * @throws \Codeception\Exception\ModuleException */ public function typeInPopup($keys) { if ($this->isPhantom()) { throw new ModuleException($this, 'PhantomJS does not support working with popups'); } $this->webDriver->switchTo()->alert()->sendKeys($keys); } /** * Reloads the current page. */ public function reloadPage() { $this->webDriver->navigate()->refresh(); } /** * Moves back in history. */ public function moveBack() { $this->webDriver->navigate()->back(); $this->debug($this->_getCurrentUri()); } /** * Moves forward in history. */ public function moveForward() { $this->webDriver->navigate()->forward(); $this->debug($this->_getCurrentUri()); } protected function getSubmissionFormFieldName($name) { if (substr($name, -2) === '[]') { return substr($name, 0, -2); } return $name; } /** * Submits the given form on the page, optionally with the given form * values. Give the form fields values as an array. Note that hidden fields * can't be accessed. * * Skipped fields will be filled by their values from the page. * You don't need to click the 'Submit' button afterwards. * This command itself triggers the request to form's action. * * You can optionally specify what button's value to include * in the request with the last parameter as an alternative to * explicitly setting its value in the second parameter, as * button values are not otherwise included in the request. * * Examples: * * ``` php * <?php * $I->submitForm('#login', [ * 'login' => 'davert', * 'password' => '123456' * ]); * // or * $I->submitForm('#login', [ * 'login' => 'davert', * 'password' => '123456' * ], 'submitButtonName'); * * ``` * * For example, given this sample "Sign Up" form: * * ``` html * <form action="/sign_up"> * Login: * <input type="text" name="user[login]" /><br/> * Password: * <input type="password" name="user[password]" /><br/> * Do you agree to our terms? * <input type="checkbox" name="user[agree]" /><br/> * Select pricing plan: * <select name="plan"> * <option value="1">Free</option> * <option value="2" selected="selected">Paid</option> * </select> * <input type="submit" name="submitButton" value="Submit" /> * </form> * ``` * * You could write the following to submit it: * * ``` php * <?php * $I->submitForm( * '#userForm', * [ * 'user[login]' => 'Davert', * 'user[password]' => '123456', * 'user[agree]' => true * ], * 'submitButton' * ); * ``` * Note that "2" will be the submitted value for the "plan" field, as it is * the selected option. * * Also note that this differs from PhpBrowser, in that * ```'user' => [ 'login' => 'Davert' ]``` is not supported at the moment. * Named array keys *must* be included in the name as above. * * Pair this with seeInFormFields for quick testing magic. * * ``` php * <?php * $form = [ * 'field1' => 'value', * 'field2' => 'another value', * 'checkbox1' => true, * // ... * ]; * $I->submitForm('//form[@id=my-form]', $form, 'submitButton'); * // $I->amOnPage('/path/to/form-page') may be needed * $I->seeInFormFields('//form[@id=my-form]', $form); * ?> * ``` * * Parameter values must be set to arrays for multiple input fields * of the same name, or multi-select combo boxes. For checkboxes, * either the string value can be used, or boolean values which will * be replaced by the checkbox's value in the DOM. * * ``` php * <?php * $I->submitForm('#my-form', [ * 'field1' => 'value', * 'checkbox' => [ * 'value of first checkbox', * 'value of second checkbox', * ], * 'otherCheckboxes' => [ * true, * false, * false, * ], * 'multiselect' => [ * 'first option value', * 'second option value', * ] * ]); * ?> * ``` * * Mixing string and boolean values for a checkbox's value is not supported * and may produce unexpected results. * * Field names ending in "[]" must be passed without the trailing square * bracket characters, and must contain an array for its value. This allows * submitting multiple values with the same name, consider: * * ```php * $I->submitForm('#my-form', [ * 'field[]' => 'value', * 'field[]' => 'another value', // 'field[]' is already a defined key * ]); * ``` * * The solution is to pass an array value: * * ```php * // this way both values are submitted * $I->submitForm('#my-form', [ * 'field' => [ * 'value', * 'another value', * ] * ]); * ``` * * The `$button` parameter can be either a string, an array or an instance * of Facebook\WebDriver\WebDriverBy. When it is a string, the * button will be found by its "name" attribute. If $button is an * array then it will be treated as a strict selector and a WebDriverBy * will be used verbatim. * * For example, given the following HTML: * * ``` html * <input type="submit" name="submitButton" value="Submit" /> * ``` * * `$button` could be any one of the following: * - 'submitButton' * - ['name' => 'submitButton'] * - WebDriverBy::name('submitButton') * * @param $selector * @param $params * @param $button */ public function submitForm($selector, array $params, $button = null) { $form = $this->matchFirstOrFail($this->getBaseElement(), $selector); $fields = $form->findElements( WebDriverBy::cssSelector('input:enabled,textarea:enabled,select:enabled,input[type=hidden]') ); foreach ($fields as $field) { $fieldName = $this->getSubmissionFormFieldName($field->getAttribute('name')); if (!isset($params[$fieldName])) { continue; } $value = $params[$fieldName]; if (is_array($value) && $field->getTagName() !== 'select') { if ($field->getAttribute('type') === 'checkbox' || $field->getAttribute('type') === 'radio') { $found = false; foreach ($value as $index => $val) { if (!is_bool($val) && $val === $field->getAttribute('value')) { array_splice($params[$fieldName], $index, 1); $value = $val; $found = true; break; } } if (!$found && !empty($value) && is_bool(reset($value))) { $value = array_pop($params[$fieldName]); } } else { $value = array_pop($params[$fieldName]); } } if ($field->getAttribute('type') === 'checkbox' || $field->getAttribute('type') === 'radio') { if ($value === true || $value === $field->getAttribute('value')) { $this->checkOption($field); } else { $this->uncheckOption($field); } } elseif ($field->getAttribute('type') === 'button' || $field->getAttribute('type') === 'submit') { continue; } elseif ($field->getTagName() === 'select') { $this->selectOption($field, $value); } else { $this->fillField($field, $value); } } $this->debugSection( 'Uri', $form->getAttribute('action') ? $form->getAttribute('action') : $this->_getCurrentUri() ); $this->debugSection('Method', $form->getAttribute('method') ? $form->getAttribute('method') : 'GET'); $this->debugSection('Parameters', json_encode($params)); $submitted = false; if (!empty($button)) { if (is_array($button)) { $buttonSelector = $this->getStrictLocator($button); } elseif ($button instanceof WebDriverBy) { $buttonSelector = $button; } else { $buttonSelector = WebDriverBy::name($button); } $els = $form->findElements($buttonSelector); if (!empty($els)) { $el = reset($els); $el->click(); $submitted = true; } } if (!$submitted) { $form->submit(); } $this->debugSection('Page', $this->_getCurrentUri()); } /** * Waits up to $timeout seconds for the given element to change. * Element "change" is determined by a callback function which is called repeatedly * until the return value evaluates to true. * * ``` php * <?php * use \Facebook\WebDriver\WebDriverElement * $I->waitForElementChange('#menu', function(WebDriverElement $el) { * return $el->isDisplayed(); * }, 100); * ?> * ``` * * @param $element * @param \Closure $callback * @param int $timeout seconds * @throws \Codeception\Exception\ElementNotFound */ public function waitForElementChange($element, \Closure $callback, $timeout = 30) { $el = $this->matchFirstOrFail($this->getBaseElement(), $element); $checker = function () use ($el, $callback) { return $callback($el); }; $this->webDriver->wait($timeout)->until($checker); } /** * Waits up to $timeout seconds for an element to appear on the page. * If the element doesn't appear, a timeout exception is thrown. * * ``` php * <?php * $I->waitForElement('#agree_button', 30); // secs * $I->click('#agree_button'); * ?> * ``` * * @param $element * @param int $timeout seconds * @throws \Exception */ public function waitForElement($element, $timeout = 10) { $condition = WebDriverExpectedCondition::presenceOfElementLocated($this->getLocator($element)); $this->webDriver->wait($timeout)->until($condition); } /** * Waits up to $timeout seconds for the given element to be visible on the page. * If element doesn't appear, a timeout exception is thrown. * * ``` php * <?php * $I->waitForElementVisible('#agree_button', 30); // secs * $I->click('#agree_button'); * ?> * ``` * * @param $element * @param int $timeout seconds * @throws \Exception */ public function waitForElementVisible($element, $timeout = 10) { $condition = WebDriverExpectedCondition::visibilityOfElementLocated($this->getLocator($element)); $this->webDriver->wait($timeout)->until($condition); } /** * Waits up to $timeout seconds for the given element to become invisible. * If element stays visible, a timeout exception is thrown. * * ``` php * <?php * $I->waitForElementNotVisible('#agree_button', 30); // secs * ?> * ``` * * @param $element * @param int $timeout seconds * @throws \Exception */ public function waitForElementNotVisible($element, $timeout = 10) { $condition = WebDriverExpectedCondition::invisibilityOfElementLocated($this->getLocator($element)); $this->webDriver->wait($timeout)->until($condition); } /** * Waits up to $timeout seconds for the given element to be clickable. * If element doesn't become clickable, a timeout exception is thrown. * * ``` php * <?php * $I->waitForElementClickable('#agree_button', 30); // secs * $I->click('#agree_button'); * ?> * ``` * * @param $element * @param int $timeout seconds * @throws \Exception */ public function waitForElementClickable($element, $timeout = 10) { $condition = WebDriverExpectedCondition::elementToBeClickable($this->getLocator($element)); $this->webDriver->wait($timeout)->until($condition); } /** * Waits up to $timeout seconds for the given string to appear on the page. * * Can also be passed a selector to search in, be as specific as possible when using selectors. * waitForText() will only watch the first instance of the matching selector / text provided. * If the given text doesn't appear, a timeout exception is thrown. * * ``` php * <?php * $I->waitForText('foo', 30); // secs * $I->waitForText('foo', 30, '.title'); // secs * ?> * ``` * * @param string $text * @param int $timeout seconds * @param string $selector optional * @throws \Exception */ public function waitForText($text, $timeout = 10, $selector = null) { $message = sprintf( 'Waited for %d secs but text %s still not found', $timeout, Locator::humanReadableString($text) ); if (!$selector) { $condition = WebDriverExpectedCondition::textToBePresentInElement(WebDriverBy::xpath('//body'), $text); $this->webDriver->wait($timeout)->until($condition, $message); return; } $condition = WebDriverExpectedCondition::textToBePresentInElement($this->getLocator($selector), $text); $this->webDriver->wait($timeout)->until($condition, $message); } /** * Wait for $timeout seconds. * * @param int|float $timeout secs * @throws \Codeception\Exception\TestRuntimeException */ public function wait($timeout) { if ($timeout >= 1000) { throw new TestRuntimeException( " Waiting for more then 1000 seconds: 16.6667 mins\n Please note that wait method accepts number of seconds as parameter." ); } usleep($timeout * 1000000); } /** * Low-level API method. * If Codeception commands are not enough, this allows you to use Selenium WebDriver methods directly: * * ``` php * $I->executeInSelenium(function(\Facebook\WebDriver\Remote\RemoteWebDriver $webdriver) { * $webdriver->get('http://google.com'); * }); * ``` * * This runs in the context of the * [RemoteWebDriver class](https://github.com/facebook/php-webdriver/blob/master/lib/remote/RemoteWebDriver.php). * Try not to use this command on a regular basis. * If Codeception lacks a feature you need, please implement it and submit a patch. * * @param callable $function */ public function executeInSelenium(\Closure $function) { return $function($this->webDriver); } /** * Switch to another window identified by name. * * The window can only be identified by name. If the $name parameter is blank, the parent window will be used. * * Example: * ``` html * <input type="button" value="Open window" onclick="window.open('http://example.com', 'another_window')"> * ``` * * ``` php * <?php * $I->click("Open window"); * # switch to another window * $I->switchToWindow("another_window"); * # switch to parent window * $I->switchToWindow(); * ?> * ``` * * If the window has no name, match it by switching to next active tab using `switchToNextTab` method. * * Or use native Selenium functions to get access to all opened windows: * * ``` php * <?php * $I->executeInSelenium(function (\Facebook\WebDriver\Remote\RemoteWebDriver $webdriver) { * $handles=$webdriver->getWindowHandles(); * $last_window = end($handles); * $webdriver->switchTo()->window($last_window); * }); * ?> * ``` * * @param string|null $name */ public function switchToWindow($name = null) { $this->webDriver->switchTo()->window($name); } /** * Switch to another frame on the page. * * Example: * ``` html * <iframe name="another_frame" src="http://example.com"> * * ``` * * ``` php * <?php * # switch to iframe * $I->switchToIFrame("another_frame"); * # switch to parent page * $I->switchToIFrame(); * * ``` * * @param string|null $name */ public function switchToIFrame($name = null) { if (is_null($name)) { $this->webDriver->switchTo()->defaultContent(); return; } try { $this->webDriver->switchTo()->frame($name); } catch (\Exception $e) { $this->debug('Iframe was not found by name, locating iframe by CSS or XPath'); $frames = $this->_findElements($name); if (!count($frames)) { throw $e; } $this->webDriver->switchTo()->frame($frames[0]); } } /** * Executes JavaScript and waits up to $timeout seconds for it to return true. * * In this example we will wait up to 60 seconds for all jQuery AJAX requests to finish. * * ``` php * <?php * $I->waitForJS("return $.active == 0;", 60); * ?> * ``` * * @param string $script * @param int $timeout seconds */ public function waitForJS($script, $timeout = 5) { $condition = function ($wd) use ($script) { return $wd->executeScript($script); }; $message = sprintf( 'Waited for %d secs but script %s still not executed', $timeout, Locator::humanReadableString($script) ); $this->webDriver->wait($timeout)->until($condition, $message); } /** * Executes custom JavaScript. * * This example uses jQuery to get a value and assigns that value to a PHP variable: * * ```php * <?php * $myVar = $I->executeJS('return $("#myField").val()'); * * // additional arguments can be passed as array * // Example shows `Hello World` alert: * $I->executeJS("window.alert(arguments[0])", ['Hello world']); * ``` * * @param $script * @param array $arguments * @return mixed */ public function executeJS($script, array $arguments = []) { return $this->webDriver->executeScript($script, $arguments); } /** * Executes asynchronous JavaScript. * A callback should be executed by JavaScript to exit from a script. * Callback is passed as a last element in `arguments` array. * Additional arguments can be passed as array in second parameter. * * ```js * // wait for 1200 milliseconds my running `setTimeout` * * $I->executeAsyncJS('setTimeout(arguments[0], 1200)'); * * $seconds = 1200; // or seconds are passed as argument * $I->executeAsyncJS('setTimeout(arguments[1], arguments[0])', [$seconds]); * ``` * * @param $script * @param array $arguments * @return mixed */ public function executeAsyncJS($script, array $arguments = []) { return $this->webDriver->executeAsyncScript($script, $arguments); } /** * Maximizes the current window. */ public function maximizeWindow() { $this->webDriver->manage()->window()->maximize(); } /** * Performs a simple mouse drag-and-drop operation. * * ``` php * <?php * $I->dragAndDrop('#drag', '#drop'); * ?> * ``` * * @param string $source (CSS ID or XPath) * @param string $target (CSS ID or XPath) */ public function dragAndDrop($source, $target) { $snodes = $this->matchFirstOrFail($this->getBaseElement(), $source); $tnodes = $this->matchFirstOrFail($this->getBaseElement(), $target); $action = new WebDriverActions($this->webDriver); $action->dragAndDrop($snodes, $tnodes)->perform(); } /** * Move mouse over the first element matched by the given locator. * If the first parameter null then the page is used. * If the second and third parameters are given, * then the mouse is moved to an offset of the element's top-left corner. * Otherwise, the mouse is moved to the center of the element. * * ``` php * <?php * $I->moveMouseOver(['css' => '.checkout']); * $I->moveMouseOver(null, 20, 50); * $I->moveMouseOver(['css' => '.checkout'], 20, 50); * ?> * ``` * * @param string $cssOrXPath css or xpath of the web element * @param int $offsetX * @param int $offsetY * * @throws \Codeception\Exception\ElementNotFound */ public function moveMouseOver($cssOrXPath = null, $offsetX = null, $offsetY = null) { $where = null; if (null !== $cssOrXPath) { $el = $this->matchFirstOrFail($this->getBaseElement(), $cssOrXPath); $where = $el->getCoordinates(); } $this->webDriver->getMouse()->mouseMove($where, $offsetX, $offsetY); } /** * Performs click with the left mouse button on an element. * If the first parameter `null` then the offset is relative to the actual mouse position. * If the second and third parameters are given, * then the mouse is moved to an offset of the element's top-left corner. * Otherwise, the mouse is moved to the center of the element. * * ``` php * <?php * $I->clickWithLeftButton(['css' => '.checkout']); * $I->clickWithLeftButton(null, 20, 50); * $I->clickWithLeftButton(['css' => '.checkout'], 20, 50); * ?> * ``` * * @param string $cssOrXPath css or xpath of the web element (body by default). * @param int $offsetX * @param int $offsetY * * @throws \Codeception\Exception\ElementNotFound */ public function clickWithLeftButton($cssOrXPath = null, $offsetX = null, $offsetY = null) { $this->moveMouseOver($cssOrXPath, $offsetX, $offsetY); $this->webDriver->getMouse()->click(); } /** * Performs contextual click with the right mouse button on an element. * If the first parameter `null` then the offset is relative to the actual mouse position. * If the second and third parameters are given, * then the mouse is moved to an offset of the element's top-left corner. * Otherwise, the mouse is moved to the center of the element. * * ``` php * <?php * $I->clickWithRightButton(['css' => '.checkout']); * $I->clickWithRightButton(null, 20, 50); * $I->clickWithRightButton(['css' => '.checkout'], 20, 50); * ?> * ``` * * @param string $cssOrXPath css or xpath of the web element (body by default). * @param int $offsetX * @param int $offsetY * * @throws \Codeception\Exception\ElementNotFound */ public function clickWithRightButton($cssOrXPath = null, $offsetX = null, $offsetY = null) { $this->moveMouseOver($cssOrXPath, $offsetX, $offsetY); $this->webDriver->getMouse()->contextClick(); } /** * Performs a double-click on an element matched by CSS or XPath. * * @param $cssOrXPath * @throws \Codeception\Exception\ElementNotFound */ public function doubleClick($cssOrXPath) { $el = $this->matchFirstOrFail($this->getBaseElement(), $cssOrXPath); $this->webDriver->getMouse()->doubleClick($el->getCoordinates()); } /** * @param $page * @param $selector * @param bool $throwMalformed * @return array */ protected function match($page, $selector, $throwMalformed = true) { if (is_array($selector)) { try { return $page->findElements($this->getStrictLocator($selector)); } catch (InvalidSelectorException $e) { throw new MalformedLocatorException(key($selector) . ' => ' . reset($selector), "Strict locator"); } catch (InvalidElementStateException $e) { if ($this->isPhantom() and $e->getResults()['status'] == 12) { throw new MalformedLocatorException( key($selector) . ' => ' . reset($selector), "Strict locator " . $e->getCode() ); } } } if ($selector instanceof WebDriverBy) { try { return $page->findElements($selector); } catch (InvalidSelectorException $e) { throw new MalformedLocatorException( sprintf( "WebDriverBy::%s('%s')", $selector->getMechanism(), $selector->getValue() ), 'WebDriver' ); } } $isValidLocator = false; $nodes = []; try { if (Locator::isID($selector)) { $isValidLocator = true; $nodes = $page->findElements(WebDriverBy::id(substr($selector, 1))); } if (Locator::isClass($selector)) { $isValidLocator = true; $nodes = $page->findElements(WebDriverBy::className(substr($selector, 1))); } if (empty($nodes) and Locator::isCSS($selector)) { $isValidLocator = true; try { $nodes = $page->findElements(WebDriverBy::cssSelector($selector)); } catch (InvalidElementStateException $e) { $nodes = $page->findElements(WebDriverBy::linkText($selector)); } } if (empty($nodes) and Locator::isXPath($selector)) { $isValidLocator = true; $nodes = $page->findElements(WebDriverBy::xpath($selector)); } } catch (InvalidSelectorException $e) { throw new MalformedLocatorException($selector); } if (!$isValidLocator and $throwMalformed) { throw new MalformedLocatorException($selector); } return $nodes; } /** * @param array $by * @return WebDriverBy */ protected function getStrictLocator(array $by) { $type = key($by); $locator = $by[$type]; switch ($type) { case 'id': return WebDriverBy::id($locator); case 'name': return WebDriverBy::name($locator); case 'css': return WebDriverBy::cssSelector($locator); case 'xpath': return WebDriverBy::xpath($locator); case 'link': return WebDriverBy::linkText($locator); case 'class': return WebDriverBy::className($locator); default: throw new MalformedLocatorException( "$by => $locator", "Strict locator can be either xpath, css, id, link, class, name: " ); } } /** * @param $page * @param $selector * @return WebDriverElement * @throws \Codeception\Exception\ElementNotFound */ protected function matchFirstOrFail($page, $selector) { $this->enableImplicitWait(); $els = $this->match($page, $selector); $this->disableImplicitWait(); if (!count($els)) { throw new ElementNotFound($selector, "CSS or XPath"); } return reset($els); } /** * Presses the given key on the given element. * To specify a character and modifier (e.g. ctrl, alt, shift, meta), pass an array for $char with * the modifier as the first element and the character as the second. * For special keys use key constants from WebDriverKeys class. * * ``` php * <?php * // <input id="page" value="old" /> * $I->pressKey('#page','a'); // => olda * $I->pressKey('#page',array('ctrl','a'),'new'); //=> new * $I->pressKey('#page',array('shift','111'),'1','x'); //=> old!!!1x * $I->pressKey('descendant-or-self::*[@id='page']','u'); //=> oldu * $I->pressKey('#name', array('ctrl', 'a'), \Facebook\WebDriver\WebDriverKeys::DELETE); //=>'' * ?> * ``` * * @param $element * @param $char string|array Can be char or array with modifier. You can provide several chars. * @throws \Codeception\Exception\ElementNotFound */ public function pressKey($element, $char) { $el = $this->matchFirstOrFail($this->getBaseElement(), $element); $args = func_get_args(); array_shift($args); $keys = []; foreach ($args as $key) { $keys[] = $this->convertKeyModifier($key); } $el->sendKeys($keys); } protected function convertKeyModifier($keys) { if (!is_array($keys)) { return $keys; } if (!isset($keys[1])) { return $keys; } list($modifier, $key) = $keys; switch ($modifier) { case 'ctrl': case 'control': return [WebDriverKeys::CONTROL, $key]; case 'alt': return [WebDriverKeys::ALT, $key]; case 'shift': return [WebDriverKeys::SHIFT, $key]; case 'meta': return [WebDriverKeys::META, $key]; } return $keys; } protected function assertNodesContain($text, $nodes, $selector = null) { $this->assertNodeConstraint($nodes, new WebDriverConstraint($text, $this->_getCurrentUri()), $selector); } protected function assertNodesNotContain($text, $nodes, $selector = null) { $this->assertNodeConstraint($nodes, new WebDriverConstraintNot($text, $this->_getCurrentUri()), $selector); } protected function assertNodeConstraint($nodes, WebDriverConstraint $constraint, $selector = null) { $message = $selector; if (is_array($selector)) { $type = key($selector); $locator = $selector[$type]; $message = $type . ':' . $locator; } $this->assertThat($nodes, $constraint, $message); } protected function assertPageContains($needle, $message = '') { $this->assertThat( htmlspecialchars_decode($this->getVisibleText()), new PageConstraint($needle, $this->_getCurrentUri()), $message ); } protected function assertPageNotContains($needle, $message = '') { $this->assertThatItsNot( htmlspecialchars_decode($this->getVisibleText()), new PageConstraint($needle, $this->_getCurrentUri()), $message ); } protected function assertPageSourceContains($needle, $message = '') { $this->assertThat( $this->webDriver->getPageSource(), new PageConstraint($needle, $this->_getCurrentUri()), $message ); } protected function assertPageSourceNotContains($needle, $message = '') { $this->assertThatItsNot( $this->webDriver->getPageSource(), new PageConstraint($needle, $this->_getCurrentUri()), $message ); } /** * Append the given text to the given element. * Can also add a selection to a select box. * * ``` php * <?php * $I->appendField('#mySelectbox', 'SelectValue'); * $I->appendField('#myTextField', 'appended'); * ?> * ``` * * @param string $field * @param string $value * @throws \Codeception\Exception\ElementNotFound */ public function appendField($field, $value) { $el = $this->findField($field); switch ($el->getTagName()) { //Multiple select case "select": $matched = false; $wdSelect = new WebDriverSelect($el); try { $wdSelect->selectByVisibleText($value); $matched = true; } catch (NoSuchElementException $e) { // exception treated at the end } try { $wdSelect->selectByValue($value); $matched = true; } catch (NoSuchElementException $e) { // exception treated at the end } if ($matched) { return; } throw new ElementNotFound(json_encode($value), "Option inside $field matched by name or value"); case "textarea": $el->sendKeys($value); return; case "div": //allows for content editable divs $el->sendKeys(WebDriverKeys::END); $el->sendKeys($value); return; //Text, Checkbox, Radio case "input": $type = $el->getAttribute('type'); if ($type == 'checkbox') { //Find by value or css,id,xpath $field = $this->findCheckable($this->getBaseElement(), $value, true); if (!$field) { throw new ElementNotFound($value, "Checkbox or Radio by Label or CSS or XPath"); } if ($field->isSelected()) { return; } $field->click(); return; } elseif ($type == 'radio') { $this->selectOption($field, $value); return; } $el->sendKeys($value); return; } throw new ElementNotFound($field, "Field by name, label, CSS or XPath"); } /** * @param $selector * @return array */ protected function matchVisible($selector) { $els = $this->match($this->getBaseElement(), $selector); $nodes = array_filter( $els, function (WebDriverElement $el) { return $el->isDisplayed(); } ); return $nodes; } /** * @param $selector * @return WebDriverBy * @throws \InvalidArgumentException */ protected function getLocator($selector) { if ($selector instanceof WebDriverBy) { return $selector; } if (is_array($selector)) { return $this->getStrictLocator($selector); } if (Locator::isID($selector)) { return WebDriverBy::id(substr($selector, 1)); } if (Locator::isCSS($selector)) { return WebDriverBy::cssSelector($selector); } if (Locator::isXPath($selector)) { return WebDriverBy::xpath($selector); } throw new \InvalidArgumentException("Only CSS or XPath allowed"); } public function saveSessionSnapshot($name) { $this->sessionSnapshots[$name] = []; foreach ($this->webDriver->manage()->getCookies() as $cookie) { if (in_array(trim($cookie['name']), [LocalServer::COVERAGE_COOKIE, LocalServer::COVERAGE_COOKIE_ERROR])) { continue; } if ($this->cookieDomainMatchesConfigUrl($cookie)) { $this->sessionSnapshots[$name][] = $cookie; } } $this->debugSection('Snapshot', "Saved \"$name\" session snapshot"); } public function loadSessionSnapshot($name) { if (!isset($this->sessionSnapshots[$name])) { return false; } foreach ($this->webDriver->manage()->getCookies() as $cookie) { if (in_array(trim($cookie['name']), [LocalServer::COVERAGE_COOKIE, LocalServer::COVERAGE_COOKIE_ERROR])) { continue; } $this->webDriver->manage()->deleteCookieNamed($cookie['name']); } foreach ($this->sessionSnapshots[$name] as $cookie) { $this->setCookie($cookie['name'], $cookie['value'], (array)$cookie); } $this->debugSection('Snapshot', "Restored \"$name\" session snapshot"); return true; } public function deleteSessionSnapshot($name) { if (isset($this->sessionSnapshots[$name])) { unset($this->sessionSnapshots[$name]); } $this->debugSection('Snapshot', "Deleted \"$name\" session snapshot"); } /** * Check if the cookie domain matches the config URL. * * @param array|Cookie $cookie * @return bool */ private function cookieDomainMatchesConfigUrl($cookie) { if (!array_key_exists('domain', $cookie)) { return true; } $setCookie = new SetCookie(); $setCookie->setDomain($cookie['domain']); return $setCookie->matchesDomain(parse_url($this->config['url'], PHP_URL_HOST)); } /** * @return bool */ protected function isPhantom() { return strpos($this->config['browser'], 'phantom') === 0; } /** * Move to the middle of the given element matched by the given locator. * Extra shift, calculated from the top-left corner of the element, * can be set by passing $offsetX and $offsetY parameters. * * ``` php * <?php * $I->scrollTo(['css' => '.checkout'], 20, 50); * ?> * ``` * * @param $selector * @param int $offsetX * @param int $offsetY */ public function scrollTo($selector, $offsetX = null, $offsetY = null) { $el = $this->matchFirstOrFail($this->getBaseElement(), $selector); $x = $el->getLocation()->getX() + $offsetX; $y = $el->getLocation()->getY() + $offsetY; $this->webDriver->executeScript("window.scrollTo($x, $y)"); } /** * Opens a new browser tab (wherever it is possible) and switches to it. * * ```php * <?php * $I->openNewTab(); * ``` * Tab is opened by using `window.open` javascript in a browser. * Please note, that adblock can restrict creating such tabs. * * Can't be used with PhantomJS * */ public function openNewTab() { $this->executeJS("window.open('about:blank','_blank');"); $this->switchToNextTab(); } /** * Closes current browser tab and switches to previous active tab. * * ```php * <?php * $I->closeTab(); * ``` * * Can't be used with PhantomJS */ public function closeTab() { $prevTab = $this->getRelativeTabHandle(-1); $this->webDriver->close(); $this->webDriver->switchTo()->window($prevTab); } /** * Switches to next browser tab. * An offset can be specified. * * ```php * <?php * // switch to next tab * $I->switchToNextTab(); * // switch to 2nd next tab * $I->switchToNextTab(2); * ``` * * Can't be used with PhantomJS * * @param int $offset 1 */ public function switchToNextTab($offset = 1) { $tab = $this->getRelativeTabHandle($offset); $this->webDriver->switchTo()->window($tab); } /** * Switches to previous browser tab. * An offset can be specified. * * ```php * <?php * // switch to previous tab * $I->switchToPreviousTab(); * // switch to 2nd previous tab * $I->switchToPreviousTab(2); * ``` * * Can't be used with PhantomJS * * @param int $offset 1 */ public function switchToPreviousTab($offset = 1) { $this->switchToNextTab(0 - $offset); } protected function getRelativeTabHandle($offset) { if ($this->isPhantom()) { throw new ModuleException($this, "PhantomJS doesn't support tab actions"); } $handle = $this->webDriver->getWindowHandle(); $handles = $this->webDriver->getWindowHandles(); $idx = array_search($handle, $handles); return $handles[($idx + $offset) % count($handles)]; } /** * Waits for element and runs a sequence of actions inside its context. * Actions can be defined with array, callback, or `Codeception\Util\ActionSequence` instance. * * Actions as array are recommended for simple to combine "waitForElement" with assertions; * `waitForElement($el)` and `see('text', $el)` can be simplified to: * * ```php * <?php * $I->performOn($el, ['see' => 'text']); * ``` * * List of actions can be pragmatically build using `Codeception\Util\ActionSequence`: * * ```php * <?php * $I->performOn('.model', ActionSequence::build() * ->see('Warning') * ->see('Are you sure you want to delete this?') * ->click('Yes') * ); * ``` * * Actions executed from array or ActionSequence will print debug output for actions, and adds an action name to * exception on failure. * * Whenever you need to define more actions a callback can be used. A WebDriver module is passed for argument: * * ```php * <?php * $I->performOn('.rememberMe', function (WebDriver $I) { * $I->see('Remember me next time'); * $I->seeElement('#LoginForm_rememberMe'); * $I->dontSee('Login'); * }); * ``` * * In 3rd argument you can set number a seconds to wait for element to appear * * @param $element * @param $actions * @param int $timeout */ public function performOn($element, $actions, $timeout = 10) { $this->waitForElement($element, $timeout); $this->setBaseElement($element); $this->debugSection('InnerText', $this->getBaseElement()->getText()); if (is_callable($actions)) { $actions($this); $this->setBaseElement(); return; } if (is_array($actions)) { $actions = ActionSequence::build()->fromArray($actions); } if (!$actions instanceof ActionSequence) { throw new \InvalidArgumentException("2nd parameter, actions should be callback, ActionSequence or array"); } $actions->run($this); $this->setBaseElement(); } protected function setBaseElement($element = null) { if ($element === null) { $this->baseElement = $this->webDriver; return; } $this->baseElement = $this->matchFirstOrFail($this->webDriver, $element); } protected function enableImplicitWait() { if (!$this->config['wait']) { return; } $this->webDriver->manage()->timeouts()->implicitlyWait($this->config['wait']); } protected function disableImplicitWait() { if (!$this->config['wait']) { return; } $this->webDriver->manage()->timeouts()->implicitlyWait(0); } } <?php namespace Codeception\Module; use Codeception\Configuration; use Codeception\Exception\ConfigurationException; use Codeception\Exception\ModuleConfigException; use Codeception\Exception\ModuleException; use Codeception\Lib\Connector\Yii2 as Yii2Connector; use Codeception\Lib\Framework; use Codeception\Lib\Interfaces\ActiveRecord; use Codeception\Lib\Interfaces\PartedModule; use Codeception\TestInterface; use Codeception\Util\Debug; use Yii; use yii\base\Event; use yii\db\ActiveRecordInterface; use yii\db\Connection; use yii\db\QueryInterface; use yii\db\Transaction; /** * This module provides integration with [Yii framework](http://www.yiiframework.com/) (2.0). * It initializes Yii framework in test environment and provides actions for functional testing. * ## Application state during testing * This section details what you can expect when using this module. * * You will get a fresh application in `\Yii::$app` at the start of each test (available in the test and in `_before()`). * * Inside your test you may change application state; however these changes will be lost when doing a request if you have enabled `recreateApplication`. * * When executing a request via one of the request functions the `request` and `response` component are both recreated. * * After a request the whole application is available for inspection / interaction. * * You may use multiple database connections, each will use a separate transaction; to prevent accidental mistakes we * will warn you if you try to connect to the same database twice but we cannot reuse the same connection. * * ## Config * * * `configFile` *required* - the path to the application config file. File should be configured for test environment and return configuration array. * * `entryUrl` - initial application url (default: http://localhost/index-test.php). * * `entryScript` - front script title (like: index-test.php). If not set - taken from entryUrl. * * `transaction` - (default: true) wrap all database connection inside a transaction and roll it back after the test. Should be disabled for acceptance testing.. * * `cleanup` - (default: true) cleanup fixtures after the test * * `ignoreCollidingDSN` - (default: false) When 2 database connections use the same DSN but different settings an exception will be thrown, set this to true to disable this behavior. * * `fixturesMethod` - (default: _fixtures) Name of the method used for creating fixtures. * * `responseCleanMethod` - (default: clear) Method for cleaning the response object. Note that this is only for multiple requests inside a single test case. * Between test casesthe whole application is always recreated * * `requestCleanMethod` - (default: recreate) Method for cleaning the request object. Note that this is only for multiple requests inside a single test case. * Between test cases the whole application is always recreated * * `recreateComponents` - (default: []) Some components change their state making them unsuitable for processing multiple requests. In production this is usually * not a problem since web apps tend to die and start over after each request. This allows you to list application components that need to be recreated before each request. * As a consequence, any components specified here should not be changed inside a test since those changes will get regarded. * You can use this module by setting params in your functional.suite.yml: * * `recreateApplication` - (default: false) whether to recreate the whole application before each request * You can use this module by setting params in your functional.suite.yml: * ```yaml * actor: FunctionalTester * modules: * enabled: * - Yii2: * configFile: 'path/to/config.php' * ``` * * ### Parts * * By default all available methods are loaded, but you can specify parts to select only needed actions and avoid conflicts. * * * `init` - use module only for initialization (for acceptance tests). * * `orm` - include only `haveRecord/grabRecord/seeRecord/dontSeeRecord` actions. * * `fixtures` - use fixtures inside tests with `haveFixtures/grabFixture/grabFixtures` actions. * * `email` - include email actions `seeEmailsIsSent/grabLastSentEmail/...` * * ### Example (`functional.suite.yml`) * * ```yaml * actor: FunctionalTester * modules: * enabled: * - Yii2: * configFile: 'config/test.php' * ``` * * ### Example (`unit.suite.yml`) * * ```yaml * actor: UnitTester * modules: * enabled: * - Asserts * - Yii2: * configFile: 'config/test.php' * part: init * ``` * * ### Example (`acceptance.suite.yml`) * * ```yaml * actor: AcceptanceTester * modules: * enabled: * - WebDriver: * url: http://127.0.0.1:8080/ * browser: firefox * - Yii2: * configFile: 'config/test.php' * part: ORM # allow to use AR methods * transaction: false # don't wrap test in transaction * cleanup: false # don't cleanup the fixtures * entryScript: index-test.php * ``` * * ## Fixtures * * This module allows to use [fixtures](http://www.yiiframework.com/doc-2.0/guide-test-fixtures.html) inside a test. There are two options for that. * Fixtures can be loaded using [haveFixtures](#haveFixtures) method inside a test: * * ```php * <?php * $I->haveFixtures(['posts' => PostsFixture::className()]); * ``` * * or, if you need to load fixtures before the test, you * can specify fixtures with `_fixtures` method of a testcase: * * ```php * <?php * // inside Cest file or Codeception\TestCase\Unit * public function _fixtures() * { * return ['posts' => PostsFixture::className()] * } * ``` * * ## URL * This module provide to use native URL formats of Yii2 for all codeception commands that use url for work. * This commands allows input like: * * ```php * <?php * $I->amOnPage(['site/view','page'=>'about']); * $I->amOnPage('index-test.php?site/index'); * $I->amOnPage('http://localhost/index-test.php?site/index'); * $I->sendAjaxPostRequest(['/user/update', 'id' => 1], ['UserForm[name]' => 'G.Hopper'); * ``` * * ## Status * * Maintainer: **samdark** * Stability: **stable** * * @property \Codeception\Lib\Connector\Yii2 $client */ class Yii2 extends Framework implements ActiveRecord, PartedModule { /** * Application config file must be set. * @var array */ protected $config = [ 'fixturesMethod' => '_fixtures', 'cleanup' => true, 'ignoreCollidingDSN' => false, 'transaction' => null, 'entryScript' => '', 'entryUrl' => 'http://localhost/index-test.php', 'responseCleanMethod' => Yii2Connector::CLEAN_CLEAR, 'requestCleanMethod' => Yii2Connector::CLEAN_RECREATE, 'recreateComponents' => [], 'recreateApplication' => false ]; protected $requiredFields = ['configFile']; /** * @var Yii2Connector\FixturesStore[] */ public $loadedFixtures = []; /** * Helper to manage database connections * @var Yii2Connector\ConnectionWatcher */ private $connectionWatcher; /** * Helper to force database transaction * @var Yii2Connector\TransactionForcer */ private $transactionForcer; /** * @var array The contents of $_SERVER upon initialization of this object. * This is only used to restore it upon object destruction. * It MUST not be used anywhere else. */ private $server; public function _initialize() { if ($this->config['transaction'] === null) { $this->config['transaction'] = $this->backupConfig['transaction'] = $this->config['cleanup']; } $this->defineConstants(); $this->server = $_SERVER; $this->initServerGlobal(); } /** * Module configuration changed inside a test. * We always re-create the application. */ protected function onReconfigure() { parent::onReconfigure(); $this->client->resetApplication(); $this->configureClient($this->config); $this->client->startApp(); } /** * Adds the required server params. * Note this is done separately from the request cycle since someone might call * `Url::to` before doing a request, which would instantiate the request component with incorrect server params. */ private function initServerGlobal() { $entryUrl = $this->config['entryUrl']; $entryFile = $this->config['entryScript'] ?: basename($entryUrl); $entryScript = $this->config['entryScript'] ?: parse_url($entryUrl, PHP_URL_PATH); $_SERVER = array_merge($_SERVER, [ 'SCRIPT_FILENAME' => $entryFile, 'SCRIPT_NAME' => $entryScript, 'SERVER_NAME' => parse_url($entryUrl, PHP_URL_HOST), 'SERVER_PORT' => parse_url($entryUrl, PHP_URL_PORT) ?: '80', 'HTTPS' => parse_url($entryUrl, PHP_URL_SCHEME) === 'https' ]); } protected function validateConfig() { parent::validateConfig(); $pathToConfig = codecept_absolute_path($this->config['configFile']); if (!is_file($pathToConfig)) { throw new ModuleConfigException( __CLASS__, "The application config file does not exist: " . $pathToConfig ); } if (!in_array($this->config['responseCleanMethod'], Yii2Connector::CLEAN_METHODS)) { throw new ModuleConfigException( __CLASS__, "The response clean method must be one of: " . implode(", ", Yii2Connector::CLEAN_METHODS) ); } if (!in_array($this->config['requestCleanMethod'], Yii2Connector::CLEAN_METHODS)) { throw new ModuleConfigException( __CLASS__, "The response clean method must be one of: " . implode(", ", Yii2Connector::CLEAN_METHODS) ); } } protected function configureClient(array $settings) { $settings['configFile'] = codecept_absolute_path($settings['configFile']); foreach ($settings as $key => $value) { if (property_exists($this->client, $key)) { $this->client->$key = $value; } } $this->client->resetApplication(); } /** * Instantiates the client based on module configuration */ protected function recreateClient() { $entryUrl = $this->config['entryUrl']; $entryFile = $this->config['entryScript'] ?: basename($entryUrl); $entryScript = $this->config['entryScript'] ?: parse_url($entryUrl, PHP_URL_PATH); $this->client = new Yii2Connector([ 'SCRIPT_FILENAME' => $entryFile, 'SCRIPT_NAME' => $entryScript, 'SERVER_NAME' => parse_url($entryUrl, PHP_URL_HOST), 'SERVER_PORT' => parse_url($entryUrl, PHP_URL_PORT) ?: '80', 'HTTPS' => parse_url($entryUrl, PHP_URL_SCHEME) === 'https' ]); $this->configureClient($this->config); } public function _before(TestInterface $test) { $this->recreateClient(); $this->client->startApp(); $this->connectionWatcher = new Yii2Connector\ConnectionWatcher(); $this->connectionWatcher->start(); // load fixtures before db transaction if ($test instanceof \Codeception\Test\Cest) { $this->loadFixtures($test->getTestClass()); } else { $this->loadFixtures($test); } $this->startTransactions(); } /** * load fixtures before db transaction * * @param mixed $test instance of test class */ private function loadFixtures($test) { $this->debugSection('Fixtures', 'Loading fixtures'); if (empty($this->loadedFixtures) && method_exists($test, $this->_getConfig('fixturesMethod')) ) { $connectionWatcher = new Yii2Connector\ConnectionWatcher(); $connectionWatcher->start(); $this->haveFixtures(call_user_func([$test, $this->_getConfig('fixturesMethod')])); $connectionWatcher->stop(); $connectionWatcher->closeAll(); } $this->debugSection('Fixtures', 'Done'); } public function _after(TestInterface $test) { $_SESSION = []; $_FILES = []; $_GET = []; $_POST = []; $_COOKIE = []; $_REQUEST = []; $this->rollbackTransactions(); if ($this->config['cleanup']) { foreach ($this->loadedFixtures as $fixture) { $fixture->unloadFixtures(); } $this->loadedFixtures = []; } if ($this->client !== null) { $this->client->resetApplication(); } if (isset($this->connectionWatcher)) { $this->connectionWatcher->stop(); $this->connectionWatcher->closeAll(); unset($this->connectionWatcher); } parent::_after($test); } protected function startTransactions() { if ($this->config['transaction']) { $this->transactionForcer = new Yii2Connector\TransactionForcer($this->config['ignoreCollidingDSN']); $this->transactionForcer->start(); } } protected function rollbackTransactions() { if (isset($this->transactionForcer)) { $this->transactionForcer->rollbackAll(); $this->transactionForcer->stop(); unset($this->transactionForcer); } } public function _parts() { return ['orm', 'init', 'fixtures', 'email']; } /** * Authorizes user on a site without submitting login form. * Use it for fast pragmatic authorization in functional tests. * * ```php * <?php * // User is found by id * $I->amLoggedInAs(1); * * // User object is passed as parameter * $admin = \app\models\User::findByUsername('admin'); * $I->amLoggedInAs($admin); * ``` * Requires `user` component to be enabled and configured. * * @param $user * @throws ModuleException */ public function amLoggedInAs($user) { try { $this->client->findAndLoginUser($user); } catch (ConfigurationException $e) { throw new ModuleException($this, $e->getMessage()); } catch (\RuntimeException $e) { throw new ModuleException($this, $e->getMessage()); } } /** * Creates and loads fixtures from a config. * Signature is the same as for `fixtures()` method of `yii\test\FixtureTrait` * * ```php * <?php * $I->haveFixtures([ * 'posts' => PostsFixture::className(), * 'user' => [ * 'class' => UserFixture::className(), * 'dataFile' => '@tests/_data/models/user.php', * ], * ]); * ``` * * Note: if you need to load fixtures before the test (probably before the cleanup transaction is started; * `cleanup` options is `true` by default), you can specify fixtures with _fixtures method of a testcase * ```php * <?php * // inside Cest file or Codeception\TestCase\Unit * public function _fixtures(){ * return [ * 'user' => [ * 'class' => UserFixture::className(), * 'dataFile' => codecept_data_dir() . 'user.php' * ] * ]; * } * ``` * instead of defining `haveFixtures` in Cest `_before` * * @param $fixtures * @part fixtures */ public function haveFixtures($fixtures) { if (empty($fixtures)) { return; } $fixturesStore = new Yii2Connector\FixturesStore($fixtures); $fixturesStore->unloadFixtures(); $fixturesStore->loadFixtures(); $this->loadedFixtures[] = $fixturesStore; } /** * Returns all loaded fixtures. * Array of fixture instances * * @part fixtures * @return array */ public function grabFixtures() { return call_user_func_array( 'array_merge', array_map( // merge all fixtures from all fixture stores function ($fixturesStore) { return $fixturesStore->getFixtures(); }, $this->loadedFixtures ) ); } /** * Gets a fixture by name. * Returns a Fixture instance. If a fixture is an instance of `\yii\test\BaseActiveFixture` a second parameter * can be used to return a specific model: * * ```php * <?php * $I->haveFixtures(['users' => UserFixture::className()]); * * $users = $I->grabFixture('users'); * * // get first user by key, if a fixture is instance of ActiveFixture * $user = $I->grabFixture('users', 'user1'); * ``` * * @param $name * @return mixed * @throws ModuleException if a fixture is not found * @part fixtures */ public function grabFixture($name, $index = null) { $fixtures = $this->grabFixtures(); if (!isset($fixtures[$name])) { throw new ModuleException($this, "Fixture $name is not loaded"); } $fixture = $fixtures[$name]; if ($index === null) { return $fixture; } if ($fixture instanceof \yii\test\BaseActiveFixture) { return $fixture->getModel($index); } throw new ModuleException($this, "Fixture $name is not an instance of ActiveFixture and can't be loaded with second parameter"); } /** * Inserts record into the database. * * ``` php * <?php * $user_id = $I->haveRecord('app\models\User', array('name' => 'Davert')); * ?> * ``` * * @param $model * @param array $attributes * @return mixed * @part orm */ public function haveRecord($model, $attributes = []) { /** @var $record \yii\db\ActiveRecord * */ $record = \Yii::createObject($model); $record->setAttributes($attributes, false); $res = $record->save(false); if (!$res) { $this->fail("Record $model was not saved"); } return $record->primaryKey; } /** * Checks that record exists in database. * * ``` php * $I->seeRecord('app\models\User', array('name' => 'davert')); * ``` * * @param $model * @param array $attributes * @part orm */ public function seeRecord($model, $attributes = []) { $record = $this->findRecord($model, $attributes); if (!$record) { $this->fail("Couldn't find $model with " . json_encode($attributes)); } $this->debugSection($model, json_encode($record)); } /** * Checks that record does not exist in database. * * ``` php * $I->dontSeeRecord('app\models\User', array('name' => 'davert')); * ``` * * @param $model * @param array $attributes * @part orm */ public function dontSeeRecord($model, $attributes = []) { $record = $this->findRecord($model, $attributes); $this->debugSection($model, json_encode($record)); if ($record) { $this->fail("Unexpectedly managed to find $model with " . json_encode($attributes)); } } /** * Retrieves record from database * * ``` php * $category = $I->grabRecord('app\models\User', array('name' => 'davert')); * ``` * * @param $model * @param array $attributes * @return mixed * @part orm */ public function grabRecord($model, $attributes = []) { return $this->findRecord($model, $attributes); } /** * @param string $model Class name * @param array $attributes * @return mixed */ protected function findRecord($model, $attributes = []) { if (!class_exists($model)) { throw new \RuntimeException("Class $model does not exist"); } $rc = new \ReflectionClass($model); if ($rc->hasMethod('find') && ($findMethod = $rc->getMethod('find')) && $findMethod->isStatic() && $findMethod->isPublic() && $findMethod->getNumberOfRequiredParameters() === 0 ) { $activeQuery = $findMethod->invoke(null); if ($activeQuery instanceof QueryInterface) { return $activeQuery->andWhere($attributes)->one(); } throw new \RuntimeException("$model::find() must return an instance of yii\db\QueryInterface"); } throw new \RuntimeException("Class $model does not have a public static find() method without required parameters"); } /** * Similar to amOnPage but accepts route as first argument and params as second * * ``` * $I->amOnRoute('site/view', ['page' => 'about']); * ``` * */ public function amOnRoute($route, array $params = []) { array_unshift($params, $route); $this->amOnPage($params); } /** * To support to use the behavior of urlManager component * for the methods like this: amOnPage(), sendAjaxRequest() and etc. * @param $method * @param $uri * @param array $parameters * @param array $files * @param array $server * @param null $content * @param bool $changeHistory * @return mixed */ protected function clientRequest($method, $uri, array $parameters = [], array $files = [], array $server = [], $content = null, $changeHistory = true) { return parent::clientRequest($method, $this->client->createUrl($uri), $parameters, $files, $server, $content, $changeHistory); } /** * Gets a component from Yii container. Throws exception if component is not available * * ```php * <?php * $mailer = $I->grabComponent('mailer'); * ``` * * @param $component * @return mixed * @throws ModuleException * @deprecated in your tests you can use \Yii::$app directly. */ public function grabComponent($component) { try { return $this->client->getComponent($component); } catch (ConfigurationException $e) { throw new ModuleException($this, $e->getMessage()); } } /** * Checks that email is sent. * * ```php * <?php * // check that at least 1 email was sent * $I->seeEmailIsSent(); * * // check that only 3 emails were sent * $I->seeEmailIsSent(3); * ``` * * @param int $num * @throws ModuleException * @part email */ public function seeEmailIsSent($num = null) { if ($num === null) { $this->assertNotEmpty($this->grabSentEmails(), 'emails were sent'); return; } $this->assertEquals($num, count($this->grabSentEmails()), 'number of sent emails is equal to ' . $num); } /** * Checks that no email was sent * * @part email */ public function dontSeeEmailIsSent() { $this->seeEmailIsSent(0); } /** * Returns array of all sent email messages. * Each message implements `yii\mail\MessageInterface` interface. * Useful to perform additional checks using `Asserts` module: * * ```php * <?php * $I->seeEmailIsSent(); * $messages = $I->grabSentEmails(); * $I->assertEquals('admin@site,com', $messages[0]->getTo()); * ``` * * @part email * @return array * @throws ModuleException */ public function grabSentEmails() { try { return $this->client->getEmails(); } catch (ConfigurationException $e) { throw new ModuleException($this, $e->getMessage()); } } /** * Returns last sent email: * * ```php * <?php * $I->seeEmailIsSent(); * $message = $I->grabLastSentEmail(); * $I->assertEquals('admin@site,com', $message->getTo()); * ``` * @part email */ public function grabLastSentEmail() { $this->seeEmailIsSent(); $messages = $this->grabSentEmails(); return end($messages); } /** * Returns a list of regex patterns for recognized domain names * * @return array */ public function getInternalDomains() { return $this->client->getInternalDomains(); } private function defineConstants() { defined('YII_DEBUG') or define('YII_DEBUG', true); defined('YII_ENV') or define('YII_ENV', 'test'); defined('YII_ENABLE_ERROR_HANDLER') or define('YII_ENABLE_ERROR_HANDLER', false); } /** * Sets a cookie and, if validation is enabled, signs it. * @param string $name The name of the cookie * @param string $value The value of the cookie * @param array $params Additional cookie params like `domain`, `path`, `expires` and `secure`. */ public function setCookie($name, $val, array $params = []) { parent::setCookie($name, $this->client->hashCookieData($name, $val), $params); } /** * This function creates the CSRF Cookie. * @param string $val The value of the CSRF token * @return string[] Returns an array containing the name of the CSRF param and the masked CSRF token. */ public function createAndSetCsrfCookie($val) { $masked = $this->client->maskToken($val); $name = $this->client->getCsrfParamName(); $this->setCookie($name, $val); return [$name, $masked]; } public function _afterSuite() { parent::_afterSuite(); codecept_debug('Suite done, restoring $_SERVER to original'); $_SERVER = $this->server; } } <?php namespace Codeception\Module; use Codeception\Lib\Framework; use Codeception\TestInterface; use Codeception\Lib\Connector\ZendExpressive as ZendExpressiveConnector; use Codeception\Lib\Interfaces\DoctrineProvider; /** * This module allows you to run tests inside Zend Expressive. * * Uses `config/container.php` file by default. * * ## Status * * * Maintainer: **Naktibalda** * * Stability: **alpha** * * ## Config * * * `container` - (default: `config/container.php`) relative path to file which returns Container * * `recreateApplicationBetweenTests` - (default: false) whether to recreate the whole application before each test * * `recreateApplicationBetweenRequests` - (default: false) whether to recreate the whole application before each request * * ## Public properties * * * application - instance of `\Zend\Expressive\Application` * * container - instance of `\Interop\Container\ContainerInterface` * * client - BrowserKit client * */ class ZendExpressive extends Framework implements DoctrineProvider { protected $config = [ 'container' => 'config/container.php', 'recreateApplicationBetweenTests' => true, 'recreateApplicationBetweenRequests' => false, ]; /** * @var \Codeception\Lib\Connector\ZendExpressive */ public $client; /** * @var \Interop\Container\ContainerInterface * @deprecated Doesn't work as expected if Application is recreated between requests */ public $container; /** * @var \Zend\Expressive\Application * @deprecated Doesn't work as expected if Application is recreated between requests */ public $application; public function _initialize() { $this->client = new ZendExpressiveConnector(); $this->client->setConfig($this->config); if ($this->config['recreateApplicationBetweenTests'] == false && $this->config['recreateApplicationBetweenRequests'] == false) { $this->application = $this->client->initApplication(); $this->container = $this->client->getContainer(); } } public function _before(TestInterface $test) { $this->client = new ZendExpressiveConnector(); $this->client->setConfig($this->config); if ($this->config['recreateApplicationBetweenTests'] != false && $this->config['recreateApplicationBetweenRequests'] == false) { $this->application = $this->client->initApplication(); $this->container = $this->client->getContainer(); } elseif (isset($this->application)) { $this->client->setApplication($this->application); } } public function _after(TestInterface $test) { //Close the session, if any are open if (session_status() == PHP_SESSION_ACTIVE) { session_write_close(); } parent::_after($test); } public function _getEntityManager() { $service = 'Doctrine\ORM\EntityManager'; if (!$this->container->has($service)) { throw new \PHPUnit\Framework\AssertionFailedError("Service $service is not available in container"); } return $this->container->get('Doctrine\ORM\EntityManager'); } } <?php namespace Codeception\Module; use Codeception\Lib\Framework; use Codeception\TestInterface; use Codeception\Configuration; use Codeception\Lib\Interfaces\DoctrineProvider; use Codeception\Lib\Interfaces\PartedModule; use Codeception\Util\ReflectionHelper; use Zend\Console\Console; use Zend\EventManager\StaticEventManager; use Codeception\Lib\Connector\ZF2 as ZF2Connector; /** * This module allows you to run tests inside Zend Framework 2 and Zend Framework 3. * * File `init_autoloader` in project's root is required by Zend Framework 2. * Uses `tests/application.config.php` config file by default. * * Note: services part and Doctrine integration is not compatible with ZF3 yet * * ## Status * * * Maintainer: **Naktibalda** * * Stability: **stable** * * ## Config * * * config: relative path to config file (default: `tests/application.config.php`) * * ## Public Properties * * * application - instance of `\Zend\Mvc\ApplicationInterface` * * db - instance of `\Zend\Db\Adapter\AdapterInterface` * * client - BrowserKit client * * ## Parts * * * services - allows to use grabServiceFromContainer and addServiceToContainer with WebDriver or PhpBrowser modules. * * Usage example: * * ```yaml * actor: AcceptanceTester * modules: * enabled: * - ZF2: * part: services * - Doctrine2: * depends: ZF2 * - WebDriver: * url: http://your-url.com * browser: phantomjs * ``` */ class ZF2 extends Framework implements DoctrineProvider, PartedModule { protected $config = [ 'config' => 'tests/application.config.php', ]; /** * @var \Zend\Mvc\ApplicationInterface */ public $application; /** * @var \Zend\Db\Adapter\AdapterInterface */ public $db; /** * @var \Codeception\Lib\Connector\ZF2 */ public $client; protected $applicationConfig; protected $queries = 0; protected $time = 0; /** * @var array Used to collect domains while recursively traversing route tree */ private $domainCollector = []; public function _initialize() { $initAutoloaderFile = Configuration::projectDir() . 'init_autoloader.php'; if (file_exists($initAutoloaderFile)) { require $initAutoloaderFile; } $this->applicationConfig = require Configuration::projectDir() . $this->config['config']; if (isset($this->applicationConfig['module_listener_options']['config_cache_enabled'])) { $this->applicationConfig['module_listener_options']['config_cache_enabled'] = false; } if (class_exists(Console::class)) { Console::overrideIsConsole(false); } //grabServiceFromContainer may need client in beforeClass hooks of modules or helpers $this->client = new ZF2Connector(); $this->client->setApplicationConfig($this->applicationConfig); } public function _before(TestInterface $test) { $this->client = new ZF2Connector(); $this->client->setApplicationConfig($this->applicationConfig); $_SERVER['REQUEST_URI'] = ''; } public function _after(TestInterface $test) { $_SESSION = []; $_GET = []; $_POST = []; $_COOKIE = []; if (class_exists('Zend\EventManager\StaticEventManager')) { // reset singleton (ZF2) StaticEventManager::resetInstance(); } $this->queries = 0; $this->time = 0; parent::_after($test); } public function _afterSuite() { unset($this->client); } public function _getEntityManager() { if (!$this->client) { $this->client = new ZF2Connector(); $this->client->setApplicationConfig($this->applicationConfig); } return $this->grabServiceFromContainer('Doctrine\ORM\EntityManager'); } /** * Grabs a service from ZF2 container. * Recommended to use for unit testing. * * ``` php * <?php * $em = $I->grabServiceFromContainer('Doctrine\ORM\EntityManager'); * ?> * ``` * * @param $service * @return mixed * @part services */ public function grabServiceFromContainer($service) { return $this->client->grabServiceFromContainer($service); } /** * Adds service to ZF2 container * @param string $name * @param object $service * @part services */ public function addServiceToContainer($name, $service) { $this->client->addServiceToContainer($name, $service); } /** * Opens web page using route name and parameters. * * ``` php * <?php * $I->amOnRoute('posts.create'); * $I->amOnRoute('posts.show', array('id' => 34)); * ?> * ``` * * @param $routeName * @param array $params */ public function amOnRoute($routeName, array $params = []) { $router = $this->client->grabServiceFromContainer('router'); $url = $router->assemble($params, ['name' => $routeName]); $this->amOnPage($url); } /** * Checks that current url matches route. * * ``` php * <?php * $I->seeCurrentRouteIs('posts.index'); * $I->seeCurrentRouteIs('posts.show', ['id' => 8])); * ?> * ``` * * @param $routeName * @param array $params */ public function seeCurrentRouteIs($routeName, array $params = []) { $router = $this->client->grabServiceFromContainer('router'); $url = $router->assemble($params, ['name' => $routeName]); $this->seeCurrentUrlEquals($url); } protected function getInternalDomains() { /** * @var Zend\Mvc\Router\Http\TreeRouteStack */ $router = $this->client->grabServiceFromContainer('router'); $this->domainCollector = []; $this->addInternalDomainsFromRoutes($router->getRoutes()); return array_unique($this->domainCollector); } private function addInternalDomainsFromRoutes($routes) { foreach ($routes as $name => $route) { if ($route instanceof \Zend\Mvc\Router\Http\Hostname || $route instanceof \Zend\Router\Http\Hostname) { $this->addInternalDomain($route); } elseif ($route instanceof \Zend\Mvc\Router\Http\Part || $route instanceof \Zend\Router\Http\Part) { $parentRoute = ReflectionHelper::readPrivateProperty($route, 'route'); if ($parentRoute instanceof \Zend\Mvc\Router\Http\Hostname || $parentRoute instanceof \Zend\Mvc\Router\Http\Hostname) { $this->addInternalDomain($parentRoute); } // this is necessary to instantiate child routes try { $route->assemble([], []); } catch (\Exception $e) { } $this->addInternalDomainsFromRoutes($route->getRoutes()); } } } private function addInternalDomain($route) { $regex = ReflectionHelper::readPrivateProperty($route, 'regex'); $this->domainCollector []= '/^' . $regex . '$/'; } public function _parts() { return ['services']; } } <?php namespace Codeception; use Codeception\Exception\ModuleException; use Codeception\Lib\Interfaces\RequiresPackage; use Codeception\Lib\ModuleContainer; use Codeception\Util\Shared\Asserts; /** * Basic class for Modules and Helpers. * You must extend from it while implementing own helpers. * * Public methods of this class start with `_` prefix in order to ignore them in actor classes. * Module contains **HOOKS** which allow to handle test execution routine. * */ abstract class Module { use Asserts; /** * @var ModuleContainer */ protected $moduleContainer; /** * By setting it to false module wan't inherit methods of parent class. * * @var bool */ public static $includeInheritedActions = true; /** * Allows to explicitly set what methods have this class. * * @var array */ public static $onlyActions = []; /** * Allows to explicitly exclude actions from module. * * @var array */ public static $excludeActions = []; /** * Allows to rename actions * * @var array */ public static $aliases = []; protected $storage = []; protected $config = []; protected $backupConfig = []; protected $requiredFields = []; /** * Module constructor. * * Requires module container (to provide access between modules of suite) and config. * * @param ModuleContainer $moduleContainer * @param array|null $config */ public function __construct(ModuleContainer $moduleContainer, $config = null) { $this->moduleContainer = $moduleContainer; $this->backupConfig = $this->config; if (is_array($config)) { $this->_setConfig($config); } } /** * Allows to define initial module config. * Can be used in `_beforeSuite` hook of Helpers or Extensions * * ```php * <?php * public function _beforeSuite($settings = []) { * $this->getModule('otherModule')->_setConfig($this->myOtherConfig); * } * ``` * * @param $config * @throws Exception\ModuleConfigException * @throws ModuleException */ public function _setConfig($config) { $this->config = $this->backupConfig = array_merge($this->config, $config); $this->validateConfig(); } /** * Allows to redefine config for a specific test. * Config is restored at the end of a test. * * ```php * <?php * // cleanup DB only for specific group of tests * public function _before(Test $test) { * if (in_array('cleanup', $test->getMetadata()->getGroups()) { * $this->getModule('Db')->_reconfigure(['cleanup' => true]); * } * } * ``` * * @param $config * @throws Exception\ModuleConfigException * @throws ModuleException */ public function _reconfigure($config) { $this->config = array_merge($this->backupConfig, $config); $this->onReconfigure(); $this->validateConfig(); } /** * HOOK to be executed when config changes with `_reconfigure`. */ protected function onReconfigure() { // update client on reconfigurations } /** * Reverts config changed by `_reconfigure` */ public function _resetConfig() { $this->config = $this->backupConfig; } /** * Validates current config for required fields and required packages. * * @throws Exception\ModuleConfigException * @throws ModuleException */ protected function validateConfig() { $fields = array_keys($this->config); if (array_intersect($this->requiredFields, $fields) != $this->requiredFields) { throw new Exception\ModuleConfigException( get_class($this), "\nOptions: " . implode(', ', $this->requiredFields) . " are required\n" . "Please, update the configuration and set all the required fields\n\n" ); } if ($this instanceof RequiresPackage) { $errorMessage = ''; foreach ($this->_requires() as $className => $package) { if (class_exists($className)) { continue; } $errorMessage .= "Class $className can't be loaded, please add $package to composer.json\n"; } if ($errorMessage) { throw new ModuleException($this, $errorMessage); } } } /** * Returns a module name for a Module, a class name for Helper * * @return string */ public function _getName() { $moduleName = '\\' . get_class($this); if (strpos($moduleName, ModuleContainer::MODULE_NAMESPACE) === 0) { return substr($moduleName, strlen(ModuleContainer::MODULE_NAMESPACE)); } return $moduleName; } /** * Checks if a module has required fields * * @return bool */ public function _hasRequiredFields() { return !empty($this->requiredFields); } /** * **HOOK** triggered after module is created and configuration is loaded */ public function _initialize() { } /** * **HOOK** executed before suite * * @param array $settings */ public function _beforeSuite($settings = []) { } /** * **HOOK** executed after suite */ public function _afterSuite() { } /** * **HOOK** executed before step * * @param Step $step */ public function _beforeStep(Step $step) { } /** * **HOOK** executed after step * * @param Step $step */ public function _afterStep(Step $step) { } /** * **HOOK** executed before test * * @param TestInterface $test */ public function _before(TestInterface $test) { } /** * **HOOK** executed after test * * @param TestInterface $test */ public function _after(TestInterface $test) { } /** * **HOOK** executed when test fails but before `_after` * * @param TestInterface $test * @param \Exception $fail */ public function _failed(TestInterface $test, $fail) { } /** * Print debug message to the screen. * * @param $message */ protected function debug($message) { codecept_debug($message); } /** * Print debug message with a title * * @param $title * @param $message */ protected function debugSection($title, $message) { if (is_array($message) or is_object($message)) { $message = stripslashes(json_encode($message)); } $this->debug("[$title] $message"); } /** * Short text message to an amount of chars * * @param $message * @param $chars * @return string */ protected function shortenMessage($message, $chars = 150) { return mb_substr($message, 0, $chars, 'utf-8'); } /** * Checks that module is enabled. * * @param $name * @return bool */ protected function hasModule($name) { return $this->moduleContainer->hasModule($name); } /** * Get all enabled modules * * @return array */ protected function getModules() { return $this->moduleContainer->all(); } /** * Get another module by its name: * * ```php * <?php * $this->getModule('WebDriver')->_findElements('.items'); * ``` * * @param $name * @return Module * @throws ModuleException */ protected function getModule($name) { if (!$this->hasModule($name)) { throw new Exception\ModuleException(__CLASS__, "Module $name couldn't be connected"); } return $this->moduleContainer->getModule($name); } /** * Get config values or specific config item. * * @param mixed $key * @return mixed the config item's value or null if it doesn't exist */ public function _getConfig($key = null) { if (!$key) { return $this->config; } if (isset($this->config[$key])) { return $this->config[$key]; } return null; } protected function scalarizeArray($array) { foreach ($array as $k => $v) { if (!is_null($v) && !is_scalar($v)) { $array[$k] = (is_array($v) || $v instanceof \ArrayAccess) ? $this->scalarizeArray($v) : (string)$v; } } return $array; } } <?php namespace Codeception; use Codeception\Event\StepEvent; use Codeception\Exception\ConditionalAssertionFailed; use Codeception\Test\Metadata; class Scenario { /** * @var TestInterface */ protected $test; /** * @var Metadata */ protected $metadata; /** * @var array */ protected $steps = []; /** * @var string */ protected $feature; protected $metaStep; /** * Constructor * * @param TestInterface $test */ public function __construct(TestInterface $test) { $this->metadata = $test->getMetadata(); $this->test = $test; } public function setFeature($feature) { $this->metadata->setFeature($feature); } public function getFeature() { return $this->metadata->getFeature(); } public function getGroups() { return $this->metadata->getGroups(); } public function current($key) { return $this->metadata->getCurrent($key); } public function runStep(Step $step) { $step->saveTrace(); if ($this->metaStep instanceof Step\Meta) { $step->setMetaStep($this->metaStep); } $this->steps[] = $step; $result = null; $this->metadata->getService('dispatcher')->dispatch(Events::STEP_BEFORE, new StepEvent($this->test, $step)); try { $result = $step->run($this->metadata->getService('modules')); } catch (ConditionalAssertionFailed $f) { $result = $this->test->getTestResultObject(); if (is_null($result)) { $this->metadata->getService('dispatcher')->dispatch(Events::STEP_AFTER, new StepEvent($this->test, $step)); throw $f; } else { $result->addFailure(clone($this->test), $f, $result->time()); } } catch (\Exception $e) { $this->metadata->getService('dispatcher')->dispatch(Events::STEP_AFTER, new StepEvent($this->test, $step)); throw $e; } $this->metadata->getService('dispatcher')->dispatch(Events::STEP_AFTER, new StepEvent($this->test, $step)); $step->executed = true; return $result; } public function addStep(Step $step) { $this->steps[] = $step; } /** * Returns the steps of this scenario. * * @return array */ public function getSteps() { return $this->steps; } public function getHtml() { $text = ''; foreach ($this->getSteps() as $step) { /** @var Step $step */ if ($step->getName() !== 'Comment') { $text .= $step->getHtml() . '<br/>'; } else { $text .= trim($step->getHumanizedArguments(), '"') . '<br/>'; } } $text = str_replace(['"\'', '\'"'], ["'", "'"], $text); $text = "<h3>" . mb_strtoupper('I want to ' . $this->getFeature(), 'utf-8') . "</h3>" . $text; return $text; } public function getText() { $text = ''; foreach ($this->getSteps() as $step) { $text .= $step->getPrefix() . "$step \r\n"; } $text = trim(str_replace(['"\'', '\'"'], ["'", "'"], $text)); $text = mb_strtoupper('I want to ' . $this->getFeature(), 'utf-8') . "\r\n\r\n" . $text . "\r\n\r\n"; return $text; } public function comment($comment) { $this->runStep(new \Codeception\Step\Comment($comment, [])); } public function skip($message = '') { throw new \PHPUnit\Framework\SkippedTestError($message); } public function incomplete($message = '') { throw new \PHPUnit\Framework\IncompleteTestError($message); } public function __call($method, $args) { // all methods were deprecated and removed from here trigger_error("Codeception: \$scenario->$method() has been deprecated and removed. Use annotations to pass scenario params", E_USER_DEPRECATED); } /** * @param Step\Meta $metaStep */ public function setMetaStep($metaStep) { $this->metaStep = $metaStep; } /** * @return Step\Meta */ public function getMetaStep() { return $this->metaStep; } } <?php namespace Codeception; use Codeception\Exception\ContentNotFound; use Codeception\Util\Debug; use Codeception\Util\Shared\Asserts; use PHPUnit\Framework\AssertionFailedError; abstract class Snapshot { use Asserts; protected $fileName; protected $dataSet; protected $refresh; /** * Should return data from current test run * * @return mixed */ abstract protected function fetchData(); /** * Performs assertion on saved data set against current dataset. * Can be overridden to implement custom assertion * * @param $data */ protected function assertData($data) { $this->assertEquals($this->dataSet, $data, 'Snapshot doesn\'t match real data'); } /** * Loads data set from file. */ protected function load() { if (!file_exists($this->getFileName())) { return; } $this->dataSet = json_decode(file_get_contents($this->getFileName())); if (!$this->dataSet) { throw new ContentNotFound("Loaded snapshot is empty"); } } /** * Saves data set to file */ protected function save() { file_put_contents($this->getFileName(), json_encode($this->dataSet)); } /** * If no filename is defined, generates one from class name * * @return string */ protected function getFileName() { if (!$this->fileName) { $this->fileName = preg_replace('/\W/', '.', get_class($this)) . '.json'; } return codecept_data_dir() . $this->fileName; } /** * Performs assertion for data sets */ public function assert() { // fetch data $data = $this->fetchData(); if (!$data) { throw new ContentNotFound("Fetched snapshot is empty."); } $this->load(); if (!$this->dataSet) { $this->printDebug('Snapshot is empty. Updating snapshot...'); $this->dataSet = $data; $this->save(); return; } try { $this->assertData($data); $this->printDebug('Data matches snapshot'); } catch (AssertionFailedError $exception) { $this->printDebug('Snapshot assertion failed'); if (!is_bool($this->refresh)) { $confirm = Debug::confirm('Should we update snapshot with fresh data? (Y/n) '); } else { $confirm = $this->refresh; } if ($confirm) { $this->dataSet = $data; $this->save(); $this->printDebug('Snapshot data updated'); return; } $this->fail($exception->getMessage()); } } /** * Force update snapshot data. * * @param bool $refresh */ public function shouldRefreshSnapshot($refresh = true) { $this->refresh = $refresh; } private function printDebug($message) { Debug::debug(get_class($this) . ': ' . $message); } } <?php namespace Codeception\Step; use Codeception\Step; class Action extends Step { } <?php namespace Codeception\Step\Argument; /** * Implemented in Step arguments where literal values need to be modified in test execution output (e.g. passwords). */ interface FormattedOutput { /** * Returns the argument's value formatted for output. * * @return string */ public function getOutput(); /** * Returns the argument's literal value. * * @return string */ public function __toString(); } <?php namespace Codeception\Step\Argument; class PasswordArgument implements FormattedOutput { /** * @var string */ private $password; public function __construct($password) { $this->password = $password; } /** * {@inheritdoc} */ public function getOutput() { return '******'; } /** * {@inheritdoc} */ public function __toString() { return $this->password; } } <?php namespace Codeception\Step; use Codeception\Step as CodeceptionStep; class Assertion extends CodeceptionStep { } <?php namespace Codeception\Step; use Codeception\Lib\ModuleContainer; use Codeception\Step as CodeceptionStep; class Comment extends CodeceptionStep { public function __toString() { return (string) $this->getAction(); } public function toString($maxLength) { return mb_strcut($this->__toString(), 0, $maxLength, 'utf-8'); } public function getHtml($highlightColor = '#732E81') { return '<strong>' . $this->getAction() . '</strong>'; } public function getPhpCode($maxLength) { return '// ' . $this->getAction(); } public function run(ModuleContainer $container = null) { // don't do anything, let's rest } public function getPrefix() { return ''; } } <?php namespace Codeception\Step; use Codeception\Step as CodeceptionStep; class Condition extends CodeceptionStep { } <?php namespace Codeception\Step; use Codeception\Exception\ConditionalAssertionFailed; use Codeception\Lib\ModuleContainer; use Codeception\Util\Template; class ConditionalAssertion extends Assertion implements GeneratedStep { public function run(ModuleContainer $container = null) { try { parent::run($container); } catch (\PHPUnit\Framework\AssertionFailedError $e) { throw new ConditionalAssertionFailed($e->getMessage(), $e->getCode(), $e); } } public function getAction() { $action = 'can' . ucfirst($this->action); $action = preg_replace('/^canDont/', 'cant', $action); return $action; } public function getHumanizedAction() { return $this->humanize($this->action . ' ' . $this->getHumanizedArguments()); } public static function getTemplate(Template $template) { $action = $template->getVar('action'); if ((0 !== strpos($action, 'see')) && (0 !== strpos($action, 'dontSee'))) { return ''; } $conditionalDoc = "* [!] Conditional Assertion: Test won't be stopped on fail\n " . $template->getVar('doc'); $prefix = 'can'; if (strpos($action, 'dontSee') === 0) { $prefix = 'cant'; $action = str_replace('dont', '', $action); } return $template ->place('doc', $conditionalDoc) ->place('action', $prefix . ucfirst($action)) ->place('step', 'ConditionalAssertion'); } public function match($name) { return 0 === strpos($name, 'see') || 0 === strpos($name, 'dontSee'); } } <?php namespace Codeception\Step; use Codeception\Step as CodeceptionStep; use Codeception\Lib\ModuleContainer; class Executor extends CodeceptionStep { protected $callable = null; public function __construct(\Closure $callable, $arguments = []) { parent::__construct('execute callable function', []); $this->callable = $callable; } public function run(ModuleContainer $container = null) { $callable = $this->callable; return $callable(); } } <?php namespace Codeception\Step; use Codeception\Util\Template; interface GeneratedStep { public static function getTemplate(Template $template); } <?php namespace Codeception\Step; use Codeception\Step as CodeceptionStep; use Codeception\Lib\ModuleContainer; class Incomplete extends CodeceptionStep { public function run(ModuleContainer $container = null) { throw new \PHPUnit\Framework\IncompleteTestError($this->getAction()); } public function __toString() { return $this->getAction(); } } <?php namespace Codeception\Step; use Codeception\Lib\ModuleContainer; use Codeception\Step as CodeceptionStep; class Meta extends CodeceptionStep { public function run(ModuleContainer $container = null) { } public function setTraceInfo($file, $line) { $this->file = $file; $this->line = $line; } public function setPrefix($actor) { $this->prefix = $actor; } public function getArgumentsAsString($maxLength = 200) { $argumentBackup = $this->arguments; $lastArgAsString = ''; $lastArg = end($this->arguments); if (is_string($lastArg) && strpos($lastArg, "\n") !== false) { $lastArgAsString = "\r\n " . str_replace("\n", "\n ", $lastArg); array_pop($this->arguments); } $result = parent::getArgumentsAsString($maxLength) . $lastArgAsString; $this->arguments = $argumentBackup; return $result; } public function setFailed($failed) { $this->failed = $failed; } } <?php namespace Codeception\Step; use Codeception\Lib\ModuleContainer; use Codeception\Util\Template; class Retry extends Assertion implements GeneratedStep { protected static $methodTemplate = <<<EOF /** * [!] Method is generated. * * {{doc}} * * Retry number and interval set by \$I->retry(); * * @see \{{module}}::{{method}}() */ public function {{action}}({{params}}) { \$retryNum = isset(\$this->retryNum) ? \$this->retryNum : 1; \$retryInterval = isset(\$this->retryInterval) ? \$this->retryInterval : 200; return \$this->getScenario()->runStep(new \Codeception\Step\Retry('{{method}}', func_get_args(), \$retryNum, \$retryInterval)); } EOF; private $retryNum; private $retryInterval; public function __construct($action, array $arguments = [], $retryNum, $retryInterval) { $this->action = $action; $this->arguments = $arguments; $this->retryNum = $retryNum; $this->retryInterval = $retryInterval; } public function run(ModuleContainer $container = null) { $retry = 0; $interval = $this->retryInterval; while (true) { try { return parent::run($container); } catch (\Exception $e) { $retry++; if ($retry > $this->retryNum) { throw $e; } codecept_debug("Retrying #$retry in ${interval}ms"); usleep($interval * 1000); $interval *= 2; } } } public static function getTemplate(Template $template) { $action = $template->getVar('action'); if ((strpos($action, 'have') === 0) || (strpos($action, 'am') === 0)) { return; // dont retry conditions } if (strpos($action, 'wait') === 0) { return; // dont retry waiters } $doc = "* Executes $action and retries on failure."; return (new Template(self::$methodTemplate)) ->place('method', $template->getVar('method')) ->place('module', $template->getVar('module')) ->place('params', $template->getVar('params')) ->place('doc', $doc) ->place('action', 'retry'. ucfirst($action)); } } <?php namespace Codeception\Step; use Codeception\Step as CodeceptionStep; use Codeception\Lib\ModuleContainer; class Skip extends CodeceptionStep { public function run(ModuleContainer $container = null) { throw new \PHPUnit\Framework\SkippedTestError($this->getAction()); } public function __toString() { return $this->getAction(); } } <?php namespace Codeception\Step; use Codeception\Lib\ModuleContainer; use Codeception\Util\Template; class TryTo extends Assertion implements GeneratedStep { public function run(ModuleContainer $container = null) { try { parent::run($container); } catch (\Exception $e) { codecept_debug("Failed to perform: {$e->getMessage()}, skipping..."); return false; } return true; } public static function getTemplate(Template $template) { $action = $template->getVar('action'); if ((strpos($action, 'have') === 0) || (strpos($action, 'am') === 0)) { return; // dont try on conditions } if (strpos($action, 'wait') === 0) { return; // dont try on waiters } if (strpos($action, 'grab') === 0) { return; // dont on grabbers } $conditionalDoc = "* [!] Test won't be stopped on fail. Error won't be logged \n " . $template->getVar('doc'); return $template ->place('doc', $conditionalDoc) ->place('action', 'tryTo' . ucfirst($action)) ->place('step', 'TryTo'); } } <?php namespace Codeception; use Codeception\Lib\ModuleContainer; use Codeception\Step\Argument\FormattedOutput; use Codeception\Step\Meta as MetaStep; use Codeception\Util\Locator; use PHPUnit\Framework\MockObject\MockObject; abstract class Step { const STACK_POSITION = 3; /** * @var string */ protected $action; /** * @var array */ protected $arguments; protected $debugOutput; public $executed = false; protected $line = null; protected $file = null; protected $prefix = 'I'; /** * @var MetaStep */ protected $metaStep = null; protected $failed = false; public function __construct($action, array $arguments = []) { $this->action = $action; $this->arguments = $arguments; } public function saveTrace() { $stack = debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT); if (count($stack) <= self::STACK_POSITION) { return; } $traceLine = $stack[self::STACK_POSITION - 1]; if (!isset($traceLine['file'])) { return; } $this->file = $traceLine['file']; $this->line = $traceLine['line']; $this->addMetaStep($traceLine, $stack); } private function isTestFile($file) { return preg_match('~[^\\'.DIRECTORY_SEPARATOR.'](Cest|Cept|Test).php$~', $file); } public function getName() { $class = explode('\\', __CLASS__); return end($class); } public function getAction() { return $this->action; } public function getLine() { if ($this->line && $this->file) { return codecept_relative_path($this->file) . ':' . $this->line; } } public function hasFailed() { return $this->failed; } public function getArguments() { return $this->arguments; } public function getArgumentsAsString($maxLength = 200) { $arguments = $this->arguments; $argumentCount = count($arguments); $totalLength = $argumentCount - 1; // count separators before adding length of individual arguments foreach ($arguments as $key => $argument) { $stringifiedArgument = $this->stringifyArgument($argument); $arguments[$key] = $stringifiedArgument; $totalLength += mb_strlen($stringifiedArgument, 'utf-8'); } if ($totalLength > $maxLength && $maxLength > 0) { //sort arguments from shortest to longest uasort($arguments, function ($arg1, $arg2) { $length1 = mb_strlen($arg1, 'utf-8'); $length2 = mb_strlen($arg2, 'utf-8'); if ($length1 === $length2) { return 0; } return ($length1 < $length2) ? -1 : 1; }); $allowedLength = floor(($maxLength - $argumentCount + 1) / $argumentCount); $lengthRemaining = $maxLength; $argumentsRemaining = $argumentCount; foreach ($arguments as $key => $argument) { $argumentsRemaining--; if (mb_strlen($argument, 'utf-8') > $allowedLength) { $arguments[$key] = mb_substr($argument, 0, $allowedLength - 4, 'utf-8') . '...' . mb_substr($argument, -1, 1, 'utf-8'); $lengthRemaining -= ($allowedLength + 1); } else { $lengthRemaining -= (mb_strlen($arguments[$key], 'utf-8') + 1); //recalculate allowed length because this argument was short if ($argumentsRemaining > 0) { $allowedLength = floor(($lengthRemaining - $argumentsRemaining + 1) / $argumentsRemaining); } } } //restore original order of arguments ksort($arguments); } return implode(',', $arguments); } protected function stringifyArgument($argument) { if (is_string($argument)) { return '"' . strtr($argument, ["\n" => '\n', "\r" => '\r', "\t" => ' ']) . '"'; } elseif (is_resource($argument)) { $argument = (string)$argument; } elseif (is_array($argument)) { foreach ($argument as $key => $value) { if (is_object($value)) { $argument[$key] = $this->getClassName($value); } } } elseif (is_object($argument)) { if ($argument instanceof FormattedOutput) { $argument = $argument->getOutput(); } elseif (method_exists($argument, '__toString')) { $argument = (string)$argument; } elseif (get_class($argument) == 'Facebook\WebDriver\WebDriverBy') { $argument = Locator::humanReadableString($argument); } else { $argument = $this->getClassName($argument); } } $arg_str = json_encode($argument, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); $arg_str = str_replace('\"', '"', $arg_str); return $arg_str; } protected function getClassName($argument) { if ($argument instanceof \Closure) { return 'Closure'; } elseif ($argument instanceof MockObject && isset($argument->__mocked)) { return $this->formatClassName($argument->__mocked); } return $this->formatClassName(get_class($argument)); } protected function formatClassName($classname) { return trim($classname, "\\"); } public function getPhpCode($maxLength) { $result = "\${$this->prefix}->" . $this->getAction() . '('; $maxLength = $maxLength - mb_strlen($result, 'utf-8') - 1; $result .= $this->getHumanizedArguments($maxLength) .')'; return $result; } /** * @return MetaStep */ public function getMetaStep() { return $this->metaStep; } public function __toString() { $humanizedAction = $this->humanize($this->getAction()); return $humanizedAction . ' ' . $this->getHumanizedArguments(); } public function toString($maxLength) { $humanizedAction = $this->humanize($this->getAction()); $maxLength = $maxLength - mb_strlen($humanizedAction, 'utf-8') - 1; return $humanizedAction . ' ' . $this->getHumanizedArguments($maxLength); } public function getHtml($highlightColor = '#732E81') { if (empty($this->arguments)) { return sprintf('%s %s', ucfirst($this->prefix), $this->humanize($this->getAction())); } return sprintf('%s %s <span style="color: %s">%s</span>', ucfirst($this->prefix), htmlspecialchars($this->humanize($this->getAction())), $highlightColor, htmlspecialchars($this->getHumanizedArguments())); } public function getHumanizedActionWithoutArguments() { return $this->humanize($this->getAction()); } public function getHumanizedArguments($maxLength = 200) { return $this->getArgumentsAsString($maxLength); } protected function clean($text) { return str_replace('\/', '', $text); } protected function humanize($text) { $text = preg_replace('/([A-Z]+)([A-Z][a-z])/', '\\1 \\2', $text); $text = preg_replace('/([a-z\d])([A-Z])/', '\\1 \\2', $text); $text = preg_replace('~\bdont\b~', 'don\'t', $text); return mb_strtolower($text, 'UTF-8'); } public function run(ModuleContainer $container = null) { $this->executed = true; if (!$container) { return null; } $activeModule = $container->moduleForAction($this->action); if (!is_callable([$activeModule, $this->action])) { throw new \RuntimeException("Action '{$this->action}' can't be called"); } try { $res = call_user_func_array([$activeModule, $this->action], $this->arguments); } catch (\Exception $e) { $this->failed = true; if ($this->getMetaStep()) { $this->getMetaStep()->setFailed(true); } throw $e; } return $res; } /** * If steps are combined into one method they can be reproduced as meta-step. * We are using stack trace to analyze if steps were called from test, if not - they were called from meta-step. * * @param $step * @param $stack */ protected function addMetaStep($step, $stack) { if (($this->isTestFile($this->file)) || ($step['class'] == 'Codeception\Scenario')) { return; } $i = count($stack) - self::STACK_POSITION - 1; // get into test file and retrieve its actual call while (isset($stack[$i])) { $step = $stack[$i]; $i--; if (!isset($step['file']) or !isset($step['function']) or !isset($step['class'])) { continue; } if (!$this->isTestFile($step['file'])) { continue; } // in case arguments were passed by reference, copy args array to ensure dereference. array_values() does not dereference values $this->metaStep = new Step\Meta($step['function'], array_map(function ($i) { return $i; }, array_values($step['args']))); $this->metaStep->setTraceInfo($step['file'], $step['line']); // pageobjects or other classes should not be included with "I" if (!in_array('Codeception\Actor', class_parents($step['class']))) { if (isset($step['object'])) { $this->metaStep->setPrefix(get_class($step['object']) . ':'); return; } $this->metaStep->setPrefix($step['class'] . ':'); } return; } } /** * @param MetaStep $metaStep */ public function setMetaStep($metaStep) { $this->metaStep = $metaStep; } /** * @return string */ public function getPrefix() { return $this->prefix . ' '; } } <?php namespace Codeception\Subscriber; use Codeception\Configuration; use Codeception\Event\SuiteEvent; use Codeception\Events; use Codeception\Lib\Generator\Actions; use Symfony\Component\EventDispatcher\EventSubscriberInterface; class AutoRebuild implements EventSubscriberInterface { use Shared\StaticEvents; public static $events = [ Events::SUITE_INIT => 'updateActor' ]; public function updateActor(SuiteEvent $e) { $settings = $e->getSettings(); if (!$settings['actor']) { codecept_debug('actor is empty'); return; // no actor } $modules = $e->getSuite()->getModules(); $actorActionsFile = Configuration::supportDir() . '_generated' . DIRECTORY_SEPARATOR . $settings['actor'] . 'Actions.php'; if (!file_exists($actorActionsFile)) { codecept_debug("Generating {$settings['actor']}Actions..."); $this->generateActorActions($actorActionsFile, $settings); return; } // load actor class to see hash $handle = @fopen($actorActionsFile, "r"); if ($handle and is_writable($actorActionsFile)) { $line = @fgets($handle); if (preg_match('~\[STAMP\] ([a-f0-9]*)~', $line, $matches)) { $hash = $matches[1]; $currentHash = Actions::genHash($modules, $settings); // regenerate actor class when hashes do not match if ($hash != $currentHash) { codecept_debug("Rebuilding {$settings['actor']}..."); @fclose($handle); $this->generateActorActions($actorActionsFile, $settings); return; } } @fclose($handle); } } protected function generateActorActions($actorActionsFile, $settings) { if (!file_exists(Configuration::supportDir() . '_generated')) { @mkdir(Configuration::supportDir() . '_generated'); } $actionsGenerator = new Actions($settings); $generated = $actionsGenerator->produce(); @file_put_contents($actorActionsFile, $generated); } } <?php namespace Codeception\Subscriber; use Codeception\Event\SuiteEvent; use Codeception\Events; use Symfony\Component\EventDispatcher\EventSubscriberInterface; class BeforeAfterTest implements EventSubscriberInterface { use Shared\StaticEvents; public static $events = [ Events::SUITE_BEFORE => 'beforeClass', Events::SUITE_AFTER => ['afterClass', 100] ]; protected $hooks = []; protected $startedTests = []; protected $unsuccessfulTests = []; public function beforeClass(SuiteEvent $e) { foreach ($e->getSuite()->tests() as $test) { /** @var $test \PHPUnit\Framework\Test * */ if ($test instanceof \PHPUnit\Framework\TestSuite\DataProvider) { $potentialTestClass = strstr($test->getName(), '::', true); $this->hooks[$potentialTestClass] = \PHPUnit\Util\Test::getHookMethods($potentialTestClass); } $testClass = get_class($test); $this->hooks[$testClass] = \PHPUnit\Util\Test::getHookMethods($testClass); } $this->runHooks('beforeClass'); } public function afterClass(SuiteEvent $e) { $this->runHooks('afterClass'); } protected function runHooks($hookName) { foreach ($this->hooks as $className => $hook) { foreach ($hook[$hookName] as $method) { if (is_callable([$className, $method])) { call_user_func([$className, $method]); } } } } } <?php namespace Codeception\Subscriber; use Codeception\Event\SuiteEvent; use Codeception\Events; use Codeception\Exception\ConfigurationException; use Symfony\Component\EventDispatcher\EventSubscriberInterface; class Bootstrap implements EventSubscriberInterface { use Shared\StaticEvents; public static $events = [ Events::SUITE_INIT => 'loadBootstrap', ]; public function loadBootstrap(SuiteEvent $e) { $settings = $e->getSettings(); if (!isset($settings['bootstrap'])) { return; } if (!$settings['bootstrap']) { return; } $bootstrap = $settings['path'] . $settings['bootstrap']; if (!is_file($bootstrap)) { throw new ConfigurationException("Bootstrap file $bootstrap can't be loaded"); } require_once $bootstrap; } } <?php namespace Codeception\Subscriber; use Codeception\Event\FailEvent; use Codeception\Event\PrintResultEvent; use Codeception\Event\StepEvent; use Codeception\Event\SuiteEvent; use Codeception\Event\TestEvent; use Codeception\Events; use Codeception\Lib\Console\Message; use Codeception\Lib\Console\MessageFactory; use Codeception\Lib\Console\Output; use Codeception\Lib\Notification; use Codeception\Step; use Codeception\Step\Comment; use Codeception\Suite; use Codeception\Test\Descriptor; use Codeception\Test\Interfaces\ScenarioDriven; use Codeception\TestInterface; use Codeception\Util\Debug; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Formatter\OutputFormatter; use Symfony\Component\EventDispatcher\EventSubscriberInterface; class Console implements EventSubscriberInterface { use Shared\StaticEvents; /** * @var string[] */ public static $events = [ Events::SUITE_BEFORE => 'beforeSuite', Events::SUITE_AFTER => 'afterSuite', Events::TEST_START => 'startTest', Events::TEST_END => 'endTest', Events::STEP_BEFORE => 'beforeStep', Events::STEP_AFTER => 'afterStep', Events::TEST_SUCCESS => 'testSuccess', Events::TEST_FAIL => 'testFail', Events::TEST_ERROR => 'testError', Events::TEST_INCOMPLETE => 'testIncomplete', Events::TEST_SKIPPED => 'testSkipped', Events::TEST_WARNING => 'testWarning', Events::TEST_FAIL_PRINT => 'printFail', Events::RESULT_PRINT_AFTER => 'afterResult', ]; /** * @var Step */ protected $metaStep; /** * @var Message */ protected $message = null; protected $steps = true; protected $debug = false; protected $ansi = true; protected $silent = false; protected $lastTestFailed = false; protected $printedTest = null; protected $rawStackTrace = false; protected $traceLength = 5; protected $width; /** * @var OutputInterface */ protected $output; protected $conditionalFails = []; protected $failedStep = []; protected $reports = []; protected $namespace = ''; protected $chars = ['success' => '+', 'fail' => 'x', 'of' => ':']; protected $options = [ 'debug' => false, 'ansi' => false, 'steps' => true, 'verbosity' => 0, 'xml' => null, 'phpunit-xml' => null, 'html' => null, 'tap' => null, 'json' => null, ]; /** * @var MessageFactory */ protected $messageFactory; public function __construct($options) { $this->prepareOptions($options); $this->output = new Output($options); $this->messageFactory = new MessageFactory($this->output); if ($this->debug) { Debug::setOutput($this->output); } $this->detectWidth(); if ($this->options['ansi'] && !$this->isWin()) { $this->chars['success'] = '✔'; $this->chars['fail'] = '✖'; } foreach (['html', 'xml', 'phpunit-xml', 'tap', 'json'] as $report) { if (!$this->options[$report]) { continue; } $path = $this->absolutePath($this->options[$report]); $this->reports[] = sprintf( "- <bold>%s</bold> report generated in <comment>file://%s</comment>", strtoupper($report), $path ); } } // triggered for scenario based tests: cept, cest public function beforeSuite(SuiteEvent $e) { $this->namespace = ""; $settings = $e->getSettings(); if (isset($settings['namespace'])) { $this->namespace = $settings['namespace']; } $this->message("%s Tests (%d) ") ->with(ucfirst($e->getSuite()->getName()), $e->getSuite()->count()) ->style('bold') ->width($this->width, '-') ->prepend("\n") ->writeln(); if ($e->getSuite() instanceof Suite) { $message = $this->message( implode( ', ', array_map( function ($module) { return $module->_getName(); }, $e->getSuite()->getModules() ) ) ); $message->style('info') ->prepend('Modules: ') ->writeln(OutputInterface::VERBOSITY_VERBOSE); } $this->message('')->width($this->width, '-')->writeln(OutputInterface::VERBOSITY_VERBOSE); } // triggered for all tests public function startTest(TestEvent $e) { $this->conditionalFails = []; $test = $e->getTest(); $this->printedTest = $test; $this->message = null; if (!$this->output->isInteractive() and !$this->isDetailed($test)) { return; } $this->writeCurrentTest($test); if ($this->isDetailed($test)) { $this->output->writeln(''); $this->message(Descriptor::getTestSignature($test)) ->style('info') ->prepend('Signature: ') ->writeln(); $this->message(codecept_relative_path(Descriptor::getTestFullName($test))) ->style('info') ->prepend('Test: ') ->writeln(); if ($this->steps) { $this->message('Scenario --')->style('comment')->writeln(); $this->output->waitForDebugOutput = false; } } } public function afterStep(StepEvent $e) { $step = $e->getStep(); if (!$step->hasFailed()) { return; } if ($step instanceof Step\ConditionalAssertion) { $this->conditionalFails[] = $step; return; } $this->failedStep[] = $step; } /** * @param PrintResultEvent $event */ public function afterResult(PrintResultEvent $event) { $result = $event->getResult(); if ($result->skippedCount() + $result->notImplementedCount() > 0 and $this->options['verbosity'] < OutputInterface::VERBOSITY_VERBOSE) { $this->output->writeln("run with `-v` to get more info about skipped or incomplete tests"); } foreach ($this->reports as $message) { $this->output->writeln($message); } } private function absolutePath($path) { if ((strpos($path, '/') === 0) or (strpos($path, ':') === 1)) { // absolute path return $path; } return codecept_output_dir() . $path; } public function testSuccess(TestEvent $e) { if ($this->isDetailed($e->getTest())) { $this->message('PASSED')->center(' ')->style('ok')->append("\n")->writeln(); return; } $this->writelnFinishedTest($e, $this->message($this->chars['success'])->style('ok')); } public function endTest(TestEvent $e) { $this->metaStep = null; $this->printedTest = null; } public function testWarning(TestEvent $e) { if ($this->isDetailed($e->getTest())) { $this->message('WARNING')->center(' ')->style('pending')->append("\n")->writeln(); return; } $this->writelnFinishedTest($e, $this->message('W')->style('pending')); } public function testFail(FailEvent $e) { if ($this->isDetailed($e->getTest())) { $this->message('FAIL')->center(' ')->style('fail')->append("\n")->writeln(); return; } $this->writelnFinishedTest($e, $this->message($this->chars['fail'])->style('fail')); } public function testError(FailEvent $e) { if ($this->isDetailed($e->getTest())) { $this->message('ERROR')->center(' ')->style('fail')->append("\n")->writeln(); return; } $this->writelnFinishedTest($e, $this->message('E')->style('fail')); } public function testSkipped(FailEvent $e) { if ($this->isDetailed($e->getTest())) { $msg = $e->getFail()->getMessage(); $this->message('SKIPPED')->append($msg ? ": $msg" : '')->center(' ')->style('pending')->writeln(); return; } $this->writelnFinishedTest($e, $this->message('S')->style('pending')); } public function testIncomplete(FailEvent $e) { if ($this->isDetailed($e->getTest())) { $msg = $e->getFail()->getMessage(); $this->message('INCOMPLETE')->append($msg ? ": $msg" : '')->center(' ')->style('pending')->writeln(); return; } $this->writelnFinishedTest($e, $this->message('I')->style('pending')); } protected function isDetailed($test) { if ($test instanceof ScenarioDriven && $this->steps) { return true; } return false; } public function beforeStep(StepEvent $e) { if (!$this->steps or !$e->getTest() instanceof ScenarioDriven) { return; } $metaStep = $e->getStep()->getMetaStep(); if ($metaStep and $this->metaStep != $metaStep) { $this->message(' ' . $metaStep->getPrefix()) ->style('bold') ->append($metaStep->__toString()) ->writeln(); } $this->metaStep = $metaStep; $this->printStep($e->getStep()); } private function printStep(Step $step) { if ($step instanceof Comment and $step->__toString() == '') { return; // don't print empty comments } $msg = $this->message(' '); if ($this->metaStep) { $msg->append(' '); } $msg->append($step->getPrefix()); $prefixLength = $msg->getLength(); if (!$this->metaStep) { $msg->style('bold'); } $maxLength = $this->width - $prefixLength; $msg->append(OutputFormatter::escape($step->toString($maxLength))); if ($this->metaStep) { $msg->style('info'); } $msg->writeln(); } public function afterSuite(SuiteEvent $e) { $this->message()->width($this->width, '-')->writeln(); $messages = Notification::all(); foreach (array_count_values($messages) as $message => $count) { if ($count > 1) { $message = $count . 'x ' . $message; } $this->output->notification($message); } } public function printFail(FailEvent $e) { $failedTest = $e->getTest(); $fail = $e->getFail(); $this->output->write($e->getCount() . ") "); $this->writeCurrentTest($failedTest, false); $this->output->writeln(''); $this->message("<error> Test </error> ") ->append(codecept_relative_path(Descriptor::getTestFullName($failedTest))) ->write(); if ($failedTest instanceof ScenarioDriven) { $this->printScenarioFail($failedTest, $fail); $this->printReports($failedTest); return; } $this->printException($fail); $this->printExceptionTrace($fail); } public function printReports(TestInterface $failedTest) { $reports = $failedTest->getMetadata()->getReports(); if (count($reports)) { $this->output->writeln('<comment>Artifacts:</comment>'); $this->output->writeln(''); } foreach ($reports as $type => $report) { $type = ucfirst($type); $this->output->writeln("$type: <debug>$report</debug>"); } } public function printException($e, $cause = null) { if ($e instanceof \PHPUnit\Framework\SkippedTestError or $e instanceof \PHPUnit\Framework_IncompleteTestError) { if ($e->getMessage()) { $this->message(OutputFormatter::escape($e->getMessage()))->prepend("\n")->writeln(); } return; } $class = $e instanceof \PHPUnit\Framework\ExceptionWrapper ? $e->getClassname() : get_class($e); if (strpos($class, 'Codeception\Exception') === 0) { $class = substr($class, strlen('Codeception\Exception\\')); } $this->output->writeln(''); $message = $this->message(OutputFormatter::escape($e->getMessage())); if ($e instanceof \PHPUnit\Framework\ExpectationFailedException) { $comparisonFailure = $e->getComparisonFailure(); if ($comparisonFailure) { $message->append($this->messageFactory->prepareComparisonFailureMessage($comparisonFailure)); } } $isFailure = $e instanceof \PHPUnit\Framework\AssertionFailedError || $class === 'PHPUnit\Framework\ExpectationFailedException' || $class === 'PHPUnit\Framework\AssertionFailedError'; if (!$isFailure) { $message->prepend("[$class] ")->block('error'); } if ($isFailure && $cause) { $cause = OutputFormatter::escape(ucfirst($cause)); $message->prepend("<error> Step </error> $cause\n<error> Fail </error> "); } $message->writeln(); } public function printScenarioFail(ScenarioDriven $failedTest, $fail) { if ($this->conditionalFails) { $failedStep = (string) array_shift($this->conditionalFails); } else { $failedStep = (string) $failedTest->getScenario()->getMetaStep(); if ($failedStep === '') { $failedStep = (string) array_shift($this->failedStep); } } $this->printException($fail, $failedStep); $this->printScenarioTrace($failedTest); if ($this->output->getVerbosity() == OutputInterface::VERBOSITY_DEBUG) { $this->printExceptionTrace($fail); return; } if (!$fail instanceof \PHPUnit\Framework\AssertionFailedError) { $this->printExceptionTrace($fail); return; } } public function printExceptionTrace($e) { static $limit = 10; if ($e instanceof \PHPUnit\Framework\SkippedTestError or $e instanceof \PHPUnit\Framework_IncompleteTestError) { return; } if ($this->rawStackTrace) { $this->message(OutputFormatter::escape(\PHPUnit\Util\Filter::getFilteredStacktrace($e, true, false)))->writeln(); return; } $trace = \PHPUnit\Util\Filter::getFilteredStacktrace($e, false); $i = 0; foreach ($trace as $step) { if ($i >= $limit) { break; } $i++; $message = $this->message($i)->prepend('#')->width(4); if (!isset($step['file'])) { foreach (['class', 'type', 'function'] as $info) { if (!isset($step[$info])) { continue; } $message->append($step[$info]); } $message->writeln(); continue; } $message->append($step['file'] . ':' . $step['line']); $message->writeln(); } $prev = $e->getPrevious(); if ($prev) { $this->printExceptionTrace($prev); } } /** * @param $failedTest */ public function printScenarioTrace(ScenarioDriven $failedTest) { $trace = array_reverse($failedTest->getScenario()->getSteps()); $length = $stepNumber = count($trace); if (!$length) { return; } $this->message("\nScenario Steps:\n")->style('comment')->writeln(); foreach ($trace as $step) { /** * @var $step Step */ if (!$step->__toString()) { continue; } $message = $this ->message($stepNumber) ->prepend(' ') ->width(strlen($length)) ->append(". "); $message->append(OutputFormatter::escape($step->getPhpCode($this->width - $message->getLength()))); if ($step->hasFailed()) { $message->style('bold'); } $line = $step->getLine(); if ($line and (!$step instanceof Comment)) { $message->append(" at <info>$line</info>"); } $stepNumber--; $message->writeln(); if (($length - $stepNumber - 1) >= $this->traceLength) { break; } } $this->output->writeln(""); } public function detectWidth() { $this->width = 60; if (!$this->isWin() && (php_sapi_name() === "cli") && (getenv('TERM')) && (getenv('TERM') != 'unknown') ) { // try to get terminal width from ENV variable (bash), see also https://github.com/Codeception/Codeception/issues/3788 if (getenv('COLUMNS')) { $this->width = getenv('COLUMNS'); } else { $this->width = (int) (`command -v tput >> /dev/null 2>&1 && tput cols`) - 2; } } elseif ($this->isWin() && (php_sapi_name() === "cli")) { exec('mode con', $output); if (isset($output[4])) { preg_match('/^ +.* +(\d+)$/', $output[4], $matches); if (!empty($matches[1])) { $this->width = (int) $matches[1]; } } } return $this->width; } private function isWin() { return strtoupper(substr(PHP_OS, 0, 3)) === 'WIN'; } /** * @param \PHPUnit\Framework\SelfDescribing $test * @param bool $inProgress */ protected function writeCurrentTest(\PHPUnit\Framework\SelfDescribing $test, $inProgress = true) { $prefix = ($this->output->isInteractive() and !$this->isDetailed($test) and $inProgress) ? '- ' : ''; $testString = Descriptor::getTestAsString($test); $testString = preg_replace('~^([^:]+):\s~', "<focus>$1{$this->chars['of']}</focus> ", $testString); $this ->message($testString) ->prepend($prefix) ->write(); } protected function writelnFinishedTest(TestEvent $event, Message $result) { $test = $event->getTest(); if ($this->isDetailed($test)) { return; } if ($this->output->isInteractive()) { $this->output->write("\x0D"); } $result->append(' ')->write(); $this->writeCurrentTest($test, false); $conditionalFailsMessage = ""; $numFails = count($this->conditionalFails); if ($numFails == 1) { $conditionalFailsMessage = "[F]"; } elseif ($numFails) { $conditionalFailsMessage = "{$numFails}x[F]"; } $conditionalFailsMessage = "<error>$conditionalFailsMessage</error> "; $this->message($conditionalFailsMessage)->write(); $this->writeTimeInformation($event); $this->output->writeln(''); } /** * @param $string * @return Message */ private function message($string = '') { return $this->messageFactory->message($string); } /** * @param TestEvent $event */ protected function writeTimeInformation(TestEvent $event) { $time = $event->getTime(); if ($time) { $this ->message(number_format(round($time, 2), 2)) ->prepend('(') ->append('s)') ->style('info') ->write(); } } /** * @param $options */ private function prepareOptions($options) { $this->options = array_merge($this->options, $options); $this->debug = $this->options['debug'] || $this->options['verbosity'] >= OutputInterface::VERBOSITY_VERY_VERBOSE; $this->steps = $this->debug || $this->options['steps']; $this->rawStackTrace = ($this->options['verbosity'] === OutputInterface::VERBOSITY_DEBUG); } } <?php namespace Codeception\Subscriber; use Codeception\Event\TestEvent; use Codeception\Test\Descriptor; use Codeception\Test\Interfaces\Dependent; use Codeception\TestInterface; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Codeception\Events; class Dependencies implements EventSubscriberInterface { use Shared\StaticEvents; static $events = [ Events::TEST_START => 'testStart', Events::TEST_SUCCESS => 'testSuccess' ]; protected $successfulTests = []; public function testStart(TestEvent $event) { $test = $event->getTest(); if (!$test instanceof Dependent) { return; } $testSignatures = $test->fetchDependencies(); foreach ($testSignatures as $signature) { if (!in_array($signature, $this->successfulTests)) { $test->getMetadata()->setSkip("This test depends on $signature to pass"); return; } } } public function testSuccess(TestEvent $event) { $test = $event->getTest(); if (!$test instanceof TestInterface) { return; } $this->successfulTests[] = Descriptor::getTestSignature($test); } } <?php namespace Codeception\Subscriber; use Codeception\Event\SuiteEvent; use Codeception\Events; use Codeception\Lib\Notification; use Symfony\Component\EventDispatcher\EventSubscriberInterface; class ErrorHandler implements EventSubscriberInterface { use Shared\StaticEvents; public static $events = [ Events::SUITE_BEFORE => 'handle', Events::SUITE_AFTER => 'onFinish' ]; /** * @var bool $stopped to keep shutdownHandler from possible looping. */ private $stopped = false; /** * @var bool $initialized to avoid double error handler substitution */ private $initialized = false; private $deprecationsInstalled = false; private $oldHandler; private $suiteFinished = false; /** * @var int stores bitmask for errors */ private $errorLevel; public function __construct() { $this->errorLevel = E_ALL & ~E_STRICT & ~E_DEPRECATED; } public function onFinish(SuiteEvent $e) { $this->suiteFinished = true; } public function handle(SuiteEvent $e) { $settings = $e->getSettings(); if ($settings['error_level']) { $this->errorLevel = eval("return {$settings['error_level']};"); } error_reporting($this->errorLevel); if ($this->initialized) { return; } // We must register shutdown function before deprecation error handler to restore previous error handler // and silence DeprecationErrorHandler yelling about 'THE ERROR HANDLER HAS CHANGED!' register_shutdown_function([$this, 'shutdownHandler']); $this->registerDeprecationErrorHandler(); $this->oldHandler = set_error_handler([$this, 'errorHandler']); $this->initialized = true; } public function errorHandler($errno, $errstr, $errfile, $errline, $context = array()) { if (E_USER_DEPRECATED === $errno) { $this->handleDeprecationError($errno, $errstr, $errfile, $errline, $context); return; } if (!(error_reporting() & $errno)) { // This error code is not included in error_reporting return false; } if (strpos($errstr, 'Cannot modify header information') !== false) { return false; } throw new \PHPUnit\Framework\Exception($errstr, $errno); } public function shutdownHandler() { if ($this->deprecationsInstalled) { restore_error_handler(); } if ($this->stopped) { return; } $this->stopped = true; $error = error_get_last(); if (!$this->suiteFinished && ( $error === null || !in_array($error['type'], [E_ERROR, E_COMPILE_ERROR, E_CORE_ERROR]) )) { throw new \RuntimeException('Command Did Not Finish Properly'); } elseif (!is_array($error)) { return; } if (error_reporting() === 0) { return; } // not fatal if (!in_array($error['type'], [E_ERROR, E_COMPILE_ERROR, E_CORE_ERROR])) { return; } echo "\n\n\nFATAL ERROR. TESTS NOT FINISHED.\n"; echo sprintf("%s \nin %s:%d\n", $error['message'], $error['file'], $error['line']); } private function registerDeprecationErrorHandler() { if (class_exists('\Symfony\Bridge\PhpUnit\DeprecationErrorHandler') && 'disabled' !== getenv('SYMFONY_DEPRECATIONS_HELPER')) { // DeprecationErrorHandler only will be installed if array('PHPUnit\Util\ErrorHandler', 'handleError') // is installed or no other error handlers are installed. // So we will remove Symfony\Component\Debug\ErrorHandler if it's installed. $old = set_error_handler('var_dump'); restore_error_handler(); if ($old && is_array($old) && count($old) > 0 && is_object($old[0]) && get_class($old[0]) === 'Symfony\Component\Debug\ErrorHandler' ) { restore_error_handler(); } $this->deprecationsInstalled = true; \Symfony\Bridge\PhpUnit\DeprecationErrorHandler::register(getenv('SYMFONY_DEPRECATIONS_HELPER')); } } private function handleDeprecationError($type, $message, $file, $line, $context) { if (!($this->errorLevel & $type)) { return; } if ($this->deprecationsInstalled && $this->oldHandler) { call_user_func($this->oldHandler, $type, $message, $file, $line, $context); return; } if (strpos($message, 'Symfony 4.3')) { // skip Symfony 4.3 deprecations return; } Notification::deprecate("$message", "$file:$line"); } } <?php namespace Codeception\Subscriber; use Codeception\Configuration; use Codeception\Event\SuiteEvent; use Codeception\Events; use Codeception\Exception\ConfigurationException; use Symfony\Component\EventDispatcher\EventDispatcher; use Symfony\Component\EventDispatcher\EventSubscriberInterface; class ExtensionLoader implements EventSubscriberInterface { use Shared\StaticEvents; public static $events = [ Events::MODULE_INIT => 'registerSuiteExtensions', Events::SUITE_AFTER => 'stopSuiteExtensions' ]; protected $config; protected $options = []; protected $globalExtensions = []; protected $suiteExtensions = []; /** * @var EventDispatcher */ protected $dispatcher; public function __construct(EventDispatcher $dispatcher) { $this->dispatcher = $dispatcher; $this->config = Configuration::config(); } public function bootGlobalExtensions($options) { $this->options = $options; $this->globalExtensions = $this->bootExtensions($this->config); } public function registerGlobalExtensions() { foreach ($this->globalExtensions as $extension) { $this->dispatcher->addSubscriber($extension); } } public function registerSuiteExtensions(SuiteEvent $e) { $suiteConfig = $e->getSettings(); $extensions = $this->bootExtensions($suiteConfig); $this->suiteExtensions = []; foreach ($extensions as $extension) { $extensionClass = get_class($extension); if (isset($this->globalExtensions[$extensionClass])) { continue; // already globally enabled } $this->dispatcher->addSubscriber($extension); $this->suiteExtensions[$extensionClass] = $extension; } } public function stopSuiteExtensions() { foreach ($this->suiteExtensions as $extension) { $this->dispatcher->removeSubscriber($extension); } $this->suiteExtensions = []; } protected function bootExtensions($config) { $extensions = []; foreach ($config['extensions']['enabled'] as $extensionClass) { if (is_array($extensionClass)) { $extensionClass = key($extensionClass); } if (!class_exists($extensionClass)) { throw new ConfigurationException( "Class `$extensionClass` is not defined. Autoload it or include into " . "'_bootstrap.php' file of 'tests' directory" ); } $extensionConfig = $this->getExtensionConfig($extensionClass, $config); $extension = new $extensionClass($extensionConfig, $this->options); if (!$extension instanceof EventSubscriberInterface) { throw new ConfigurationException( "Class $extensionClass is not an EventListener. Please create it as Extension or GroupObject." ); } $extensions[get_class($extension)] = $extension; } return $extensions; } private function getExtensionConfig($extension, $config) { $extensionConfig = isset($config['extensions']['config'][$extension]) ? $config['extensions']['config'][$extension] : []; if (!isset($config['extensions']['enabled'])) { return $extensionConfig; } if (!is_array($config['extensions']['enabled'])) { return $extensionConfig; } foreach ($config['extensions']['enabled'] as $enabledExtensionsConfig) { if (!is_array($enabledExtensionsConfig)) { continue; } $enabledExtension = key($enabledExtensionsConfig); if ($enabledExtension === $extension) { return Configuration::mergeConfigs(reset($enabledExtensionsConfig), $extensionConfig); } } return $extensionConfig; } } <?php namespace Codeception\Subscriber; use Codeception\Event\SuiteEvent; use Codeception\Events; use Symfony\Component\EventDispatcher\EventSubscriberInterface; class FailFast implements EventSubscriberInterface { use Shared\StaticEvents; public static $events = [ Events::SUITE_BEFORE => 'stopOnFail', ]; public function stopOnFail(SuiteEvent $e) { $e->getResult()->stopOnError(true); $e->getResult()->stopOnFailure(true); } } <?php declare (ticks = 1); namespace Codeception\Subscriber; use Codeception\Event\SuiteEvent; use Codeception\Events; use Symfony\Component\EventDispatcher\EventSubscriberInterface; class GracefulTermination implements EventSubscriberInterface { const SIGNAL_FUNC = 'pcntl_signal'; const ASYNC_SIGNAL_HANDLING_FUNC = 'pcntl_async_signals'; /** * @var SuiteEvent */ protected $suiteEvent; public function handleSuite(SuiteEvent $event) { if (PHP_MAJOR_VERSION === 7 && PHP_MINOR_VERSION === 0) { // skip for PHP 7.0: https://github.com/Codeception/Codeception/issues/3607 return; } if (function_exists(self::ASYNC_SIGNAL_HANDLING_FUNC)) { pcntl_async_signals(true); } if (function_exists(self::SIGNAL_FUNC)) { pcntl_signal(SIGTERM, [$this, 'terminate']); pcntl_signal(SIGINT, [$this, 'terminate']); } $this->suiteEvent = $event; } public function terminate() { if ($this->suiteEvent) { $this->suiteEvent->getResult()->stopOnError(true); $this->suiteEvent->getResult()->stopOnFailure(true); } throw new \RuntimeException( "\n\n---------------------------\nTESTS EXECUTION TERMINATED\n---------------------------\n" ); } public static function getSubscribedEvents() { if (!function_exists(self::SIGNAL_FUNC)) { return []; } return [Events::SUITE_BEFORE => 'handleSuite']; } } <?php namespace Codeception\Subscriber; use Codeception\Event\FailEvent; use Codeception\Event\StepEvent; use Codeception\Event\SuiteEvent; use Codeception\Event\TestEvent; use Codeception\Events; use Codeception\Suite; use Codeception\TestInterface; use Symfony\Component\EventDispatcher\EventSubscriberInterface; class Module implements EventSubscriberInterface { use Shared\StaticEvents; public static $events = [ Events::TEST_BEFORE => 'before', Events::TEST_AFTER => 'after', Events::STEP_BEFORE => 'beforeStep', Events::STEP_AFTER => 'afterStep', Events::TEST_FAIL => 'failed', Events::TEST_ERROR => 'failed', Events::SUITE_BEFORE => 'beforeSuite', Events::SUITE_AFTER => 'afterSuite' ]; protected $modules = []; public function beforeSuite(SuiteEvent $e) { $suite = $e->getSuite(); if (!$suite instanceof Suite) { return; } $this->modules = $suite->getModules(); foreach ($this->modules as $module) { $module->_beforeSuite($e->getSettings()); } } public function afterSuite() { foreach ($this->modules as $module) { $module->_afterSuite(); } } public function before(TestEvent $event) { if (!$event->getTest() instanceof TestInterface) { return; } foreach ($this->modules as $module) { $module->_before($event->getTest()); } } public function after(TestEvent $e) { if (!$e->getTest() instanceof TestInterface) { return; } foreach ($this->modules as $module) { $module->_after($e->getTest()); $module->_resetConfig(); } } public function failed(FailEvent $e) { if (!$e->getTest() instanceof TestInterface) { return; } foreach ($this->modules as $module) { $module->_failed($e->getTest(), $e->getFail()); } } public function beforeStep(StepEvent $e) { foreach ($this->modules as $module) { $module->_beforeStep($e->getStep(), $e->getTest()); } } public function afterStep(StepEvent $e) { foreach ($this->modules as $module) { $module->_afterStep($e->getStep(), $e->getTest()); } } } <?php namespace Codeception\Subscriber; use Codeception\Event\TestEvent; use Codeception\Events; use Codeception\Lib\Di; use Codeception\Test\Cest; use Codeception\Test\Unit; use Symfony\Component\EventDispatcher\EventSubscriberInterface; class PrepareTest implements EventSubscriberInterface { use Shared\StaticEvents; public static $events = [ Events::TEST_BEFORE => 'prepare', ]; protected $modules = []; public function prepare(TestEvent $event) { $test = $event->getTest(); /** @var $di Di **/ $prepareMethods = $test->getMetadata()->getParam('prepare'); if (!$prepareMethods) { return; } $di = $test->getMetadata()->getService('di'); foreach ($prepareMethods as $method) { /** @var $module \Codeception\Module **/ if ($test instanceof Cest) { $di->injectDependencies($test->getTestClass(), $method); } if ($test instanceof Unit) { $di->injectDependencies($test, $method); } } } } <?php namespace Codeception\Subscriber\Shared; trait StaticEvents { public static function getSubscribedEvents() { return static::$events; } } <?php namespace Codeception; use Codeception\Test\Descriptor; use Codeception\Test\Interfaces\Dependent; class Suite extends \PHPUnit\Framework\TestSuite { protected $modules; protected $baseName; public function reorderDependencies() { $tests = []; foreach ($this->tests as $test) { $tests = array_merge($tests, $this->getDependencies($test)); } $queue = []; $hashes = []; foreach ($tests as $test) { if (in_array(spl_object_hash($test), $hashes)) { continue; } $hashes[] = spl_object_hash($test); $queue[] = $test; } $this->tests = $queue; } protected function getDependencies($test) { if (!$test instanceof Dependent) { return [$test]; } $tests = []; foreach ($test->fetchDependencies() as $requiredTestName) { $required = $this->findMatchedTest($requiredTestName); if (!$required) { continue; } $tests = array_merge($tests, $this->getDependencies($required)); } $tests[] = $test; return $tests; } protected function findMatchedTest($testSignature) { foreach ($this->tests as $test) { $signature = Descriptor::getTestSignature($test); if ($signature === $testSignature) { return $test; } } } /** * @return mixed */ public function getModules() { return $this->modules; } /** * @param mixed $modules */ public function setModules($modules) { $this->modules = $modules; } /** * @return mixed */ public function getBaseName() { return $this->baseName; } /** * @param mixed $baseName */ public function setBaseName($baseName) { $this->baseName = $baseName; } } <?php namespace Codeception; use Codeception\Lib\Di; use Codeception\Lib\GroupManager; use Codeception\Lib\ModuleContainer; use Codeception\Lib\Notification; use Codeception\Test\Interfaces\ScenarioDriven; use Codeception\Test\Loader; use Codeception\Test\Descriptor; use Symfony\Component\EventDispatcher\EventDispatcher; class SuiteManager { public static $environment; public static $name; /** * @var \PHPUnit\Framework\TestSuite */ protected $suite = null; /** * @var null|\Symfony\Component\EventDispatcher\EventDispatcher */ protected $dispatcher = null; /** * @var GroupManager */ protected $groupManager; /** * @var Loader */ protected $testLoader; /** * @var ModuleContainer */ protected $moduleContainer; /** * @var Di */ protected $di; protected $tests = []; protected $debug = false; protected $path = ''; protected $printer = null; protected $env = null; protected $settings; public function __construct(EventDispatcher $dispatcher, $name, array $settings) { $this->settings = $settings; $this->dispatcher = $dispatcher; $this->di = new Di(); $this->path = $settings['path']; $this->groupManager = new GroupManager($settings['groups']); $this->moduleContainer = new ModuleContainer($this->di, $settings); $modules = Configuration::modules($this->settings); foreach ($modules as $moduleName) { $this->moduleContainer->create($moduleName); } $this->moduleContainer->validateConflicts(); if (isset($settings['current_environment'])) { $this->env = $settings['current_environment']; } $this->suite = $this->createSuite($name); } public function initialize() { $this->dispatcher->dispatch(Events::MODULE_INIT, new Event\SuiteEvent($this->suite, null, $this->settings)); foreach ($this->moduleContainer->all() as $module) { $module->_initialize(); } if ($this->settings['actor'] && !file_exists(Configuration::supportDir() . $this->settings['actor'] . '.php')) { throw new Exception\ConfigurationException( $this->settings['actor'] . " class doesn't exist in suite folder.\nRun the 'build' command to generate it" ); } $this->dispatcher->dispatch(Events::SUITE_INIT, new Event\SuiteEvent($this->suite, null, $this->settings)); ini_set('xdebug.show_exception_trace', 0); // Issue https://github.com/symfony/symfony/issues/7646 } public function loadTests($path = null) { $testLoader = new Loader($this->settings); $testLoader->loadTests($path); $tests = $testLoader->getTests(); if ($this->settings['shuffle']) { shuffle($tests); } foreach ($tests as $test) { $this->addToSuite($test); } $this->suite->reorderDependencies(); } protected function addToSuite($test) { $this->configureTest($test); if ($test instanceof \PHPUnit\Framework\DataProviderTestSuite) { foreach ($test->tests() as $t) { $this->addToSuite($t); } return; } if ($test instanceof TestInterface) { $this->checkEnvironmentExists($test); if (!$this->isExecutedInCurrentEnvironment($test)) { return; // skip tests from other environments } } $groups = $this->groupManager->groupsForTest($test); $this->suite->addTest($test, $groups); if (!empty($groups) && $test instanceof TestInterface) { $test->getMetadata()->setGroups($groups); } } protected function createSuite($name) { $suite = new Suite(); $suite->setBaseName(preg_replace('~\s.+$~', '', $name)); // replace everything after space (env name) if ($this->settings['namespace']) { $name = $this->settings['namespace'] . ".$name"; } $suite->setName($name); if (isset($this->settings['backup_globals'])) { $suite->setBackupGlobals((bool) $this->settings['backup_globals']); } if (isset($this->settings['be_strict_about_changes_to_global_state']) && method_exists($suite, 'setbeStrictAboutChangesToGlobalState')) { $suite->setbeStrictAboutChangesToGlobalState((bool)$this->settings['be_strict_about_changes_to_global_state']); } $suite->setModules($this->moduleContainer->all()); return $suite; } public function run(PHPUnit\Runner $runner, \PHPUnit\Framework\TestResult $result, $options) { $runner->prepareSuite($this->suite, $options); $this->dispatcher->dispatch(Events::SUITE_BEFORE, new Event\SuiteEvent($this->suite, $result, $this->settings)); try { $runner->doEnhancedRun($this->suite, $result, $options); } finally { $this->dispatcher->dispatch(Events::SUITE_AFTER, new Event\SuiteEvent($this->suite, $result, $this->settings)); } } /** * @return \Codeception\Suite */ public function getSuite() { return $this->suite; } /** * @return ModuleContainer */ public function getModuleContainer() { return $this->moduleContainer; } protected function getActor() { if (!$this->settings['actor']) { return null; } return $this->settings['namespace'] ? rtrim($this->settings['namespace'], '\\') . '\\' . $this->settings['actor'] : $this->settings['actor']; } protected function checkEnvironmentExists(TestInterface $test) { $envs = $test->getMetadata()->getEnv(); if (empty($envs)) { return; } if (!isset($this->settings['env'])) { Notification::warning("Environments are not configured", Descriptor::getTestFullName($test)); return; } $availableEnvironments = array_keys($this->settings['env']); $listedEnvironments = explode(',', implode(',', $envs)); foreach ($listedEnvironments as $env) { if (!in_array($env, $availableEnvironments)) { Notification::warning("Environment $env was not configured but used in test", Descriptor::getTestFullName($test)); } } } protected function isExecutedInCurrentEnvironment(TestInterface $test) { $envs = $test->getMetadata()->getEnv(); if (empty($envs)) { return true; } $currentEnvironments = explode(',', $this->env); foreach ($envs as $envList) { $envList = explode(',', $envList); if (count($envList) == count(array_intersect($currentEnvironments, $envList))) { return true; } } return false; } /** * @param $t * @throws Exception\InjectionException */ protected function configureTest($t) { if (!$t instanceof TestInterface) { return; } $t->getMetadata()->setServices([ 'di' => clone($this->di), 'dispatcher' => $this->dispatcher, 'modules' => $this->moduleContainer ]); $t->getMetadata()->setCurrent([ 'actor' => $this->getActor(), 'env' => $this->env, 'modules' => $this->moduleContainer->all() ]); if ($t instanceof ScenarioDriven) { $t->preload(); } } } <?php namespace Codeception\Template; use Codeception\InitTemplate; use Codeception\Util\Template; use Symfony\Component\Yaml\Yaml; class Acceptance extends InitTemplate { protected $configTemplate = <<<EOF # suite config suites: acceptance: actor: AcceptanceTester path: . modules: enabled: - WebDriver: url: {{url}} browser: {{browser}} - \Helper\Acceptance # add Codeception\Step\Retry trait to AcceptanceTester to enable retries step_decorators: - Codeception\Step\ConditionalAssertion - Codeception\Step\TryTo - Codeception\Step\Retry extensions: enabled: [Codeception\Extension\RunFailed] params: - env gherkin: [] # additional paths paths: tests: {{baseDir}} output: {{baseDir}}/_output data: {{baseDir}}/_data support: {{baseDir}}/_support envs: {{baseDir}}/_envs settings: shuffle: false lint: true EOF; protected $firstTest = <<<EOF <?php class LoginCest { public function _before(AcceptanceTester \$I) { \$I->amOnPage('/'); } public function loginSuccessfully(AcceptanceTester \$I) { // write a positive login test } public function loginWithInvalidPassword(AcceptanceTester \$I) { // write a negative login test } } EOF; public function setup() { $this->checkInstalled(); $this->say("Let's prepare Codeception for acceptance testing"); $this->say("Create your tests and run them in real browser"); $this->say(""); $dir = $this->ask("Where tests will be stored?", 'tests'); $browser = $this->ask("Select a browser for testing", ['chrome', 'phantomjs', 'firefox']); if ($browser === 'phantomjs') { $this->sayInfo("Ensure that you have Phantomjs running before starting tests"); } if ($browser === 'chrome') { $this->sayInfo("Ensure that you have Selenium Server and ChromeDriver installed before running tests"); } if ($browser === 'firefox') { $this->sayInfo("Ensure that you have Selenium Server and GeckoDriver installed before running tests"); } $url = $this->ask("Start url for tests", "http://localhost"); $this->createEmptyDirectory($outputDir = $dir . DIRECTORY_SEPARATOR . '_output'); $this->createEmptyDirectory($dir . DIRECTORY_SEPARATOR . '_data'); $this->createDirectoryFor($supportDir = $dir . DIRECTORY_SEPARATOR . '_support'); $this->createDirectoryFor($supportDir . DIRECTORY_SEPARATOR . '_generated'); $this->gitIgnore($outputDir); $this->gitIgnore($supportDir . DIRECTORY_SEPARATOR . '_generated'); $this->sayInfo("Created test directories inside at $dir"); $configFile = (new Template($this->configTemplate)) ->place('url', $url) ->place('browser', $browser) ->place('baseDir', $dir) ->produce(); if ($this->namespace) { $namespace = rtrim($this->namespace, '\\'); $configFile = "namespace: $namespace\n" . $configFile; } $this->createFile('codeception.yml', $configFile); $this->createHelper('Acceptance', $supportDir); $this->createActor('AcceptanceTester', $supportDir, Yaml::parse($configFile)['suites']['acceptance']); $this->sayInfo("Created global config codeception.yml inside the root directory"); $this->createFile($dir . DIRECTORY_SEPARATOR . 'LoginCest.php', $this->firstTest); $this->sayInfo("Created a demo test LoginCest.php"); $this->say(); $this->saySuccess("INSTALLATION COMPLETE"); $this->say(); $this->say("<bold>Next steps:</bold>"); $this->say('1. Launch Selenium Server and webserver'); $this->say("2. Edit <bold>$dir/LoginCest.php</bold> to test login of your application"); $this->say("3. Run tests using: <comment>codecept run</comment>"); $this->say(); $this->say("HINT: Add '\\Codeception\\Step\\Retry' trait to AcceptanceTester class to enable auto-retries"); $this->say("HINT: See https://codeception.com/docs/03-AcceptanceTests#retry"); $this->say("<bold>Happy testing!</bold>"); } } <?php namespace Codeception\Template; use Codeception\InitTemplate; use Codeception\Util\Template; use Symfony\Component\Yaml\Yaml; class Api extends InitTemplate { protected $configTemplate = <<<EOF # suite config suites: api: actor: ApiTester path: . modules: enabled: - REST: url: {{url}} depends: PhpBrowser paths: tests: {{baseDir}} output: {{baseDir}}/_output data: {{baseDir}}/_data support: {{baseDir}}/_support settings: shuffle: false lint: true EOF; protected $firstTest = <<<EOF <?php class ApiCest { public function tryApi(ApiTester \$I) { \$I->sendGET('/'); \$I->seeResponseCodeIs(200); \$I->seeResponseIsJson(); } } EOF; public function setup() { $this->checkInstalled(); $this->say("Let's prepare Codeception for REST API testing"); $this->say(""); $dir = $this->ask("Where tests will be stored?", 'tests'); $url = $this->ask("Start url for tests", "http://localhost/api"); $this->createEmptyDirectory($outputDir = $dir . DIRECTORY_SEPARATOR . '_output'); $this->createEmptyDirectory($dir . DIRECTORY_SEPARATOR . '_data'); $this->createDirectoryFor($supportDir = $dir . DIRECTORY_SEPARATOR . '_support'); $this->createDirectoryFor($supportDir . DIRECTORY_SEPARATOR . '_generated'); $this->gitIgnore($outputDir); $this->gitIgnore($supportDir . DIRECTORY_SEPARATOR . '_generated'); $this->sayInfo("Created test directories inside at $dir"); $configFile = (new Template($this->configTemplate)) ->place('url', $url) ->place('baseDir', $dir) ->produce(); if ($this->namespace) { $namespace = rtrim($this->namespace, '\\'); $configFile = "namespace: $namespace\n" . $configFile; } $this->createFile('codeception.yml', $configFile); $this->createHelper('Api', $supportDir); $this->createActor('ApiTester', $supportDir, Yaml::parse($configFile)['suites']['api']); $this->sayInfo("Created global config codeception.yml inside the root directory"); $this->createFile($dir . DIRECTORY_SEPARATOR . 'ApiCest.php', $this->firstTest); $this->sayInfo("Created a demo test ApiCest.php"); $this->say(); $this->saySuccess("INSTALLATION COMPLETE"); $this->say(); $this->say("<bold>Next steps:</bold>"); $this->say("1. Edit <bold>$dir/ApiCest.php</bold> to write first API tests"); $this->say("2. Run tests using: <comment>codecept run</comment>"); $this->say(); $this->say("<bold>Happy testing!</bold>"); } } <?php namespace Codeception\Template; use Codeception\InitTemplate; use Symfony\Component\Yaml\Yaml; class Bootstrap extends InitTemplate { // defaults protected $supportDir = 'tests/_support'; protected $outputDir = 'tests/_output'; protected $dataDir = 'tests/_data'; protected $envsDir = 'tests/_envs'; public function setup() { $this->checkInstalled($this->workDir); $input = $this->input; if ($input->getOption('namespace')) { $this->namespace = trim($input->getOption('namespace'), '\\') . '\\'; } if ($input->hasOption('actor') && $input->getOption('actor')) { $this->actorSuffix = $input->getOption('actor'); } $this->say( "<fg=white;bg=magenta> Bootstrapping Codeception </fg=white;bg=magenta>\n" ); $this->createGlobalConfig(); $this->say("File codeception.yml created <- global configuration"); $this->createDirs(); if ($input->hasOption('empty') && $input->getOption('empty')) { return; } $this->createUnitSuite(); $this->say("tests/unit created <- unit tests"); $this->say("tests/unit.suite.yml written <- unit tests suite configuration"); $this->createFunctionalSuite(); $this->say("tests/functional created <- functional tests"); $this->say("tests/functional.suite.yml written <- functional tests suite configuration"); $this->createAcceptanceSuite(); $this->say("tests/acceptance created <- acceptance tests"); $this->say("tests/acceptance.suite.yml written <- acceptance tests suite configuration"); $this->say(" --- "); $this->say(); $this->saySuccess('Codeception is installed for acceptance, functional, and unit testing'); $this->say(); $this->say("<bold>Next steps:</bold>"); $this->say('1. Edit <bold>tests/acceptance.suite.yml</bold> to set url of your application. Change PhpBrowser to WebDriver to enable browser testing'); $this->say("2. Edit <bold>tests/functional.suite.yml</bold> to enable a framework module. Remove this file if you don't use a framework"); $this->say("3. Create your first acceptance tests using <comment>codecept g:cest acceptance First</comment>"); $this->say("4. Write first test in <bold>tests/acceptance/FirstCest.php</bold>"); $this->say("5. Run tests using: <comment>codecept run</comment>"); } protected function createDirs() { $this->createDirectoryFor('tests'); $this->createEmptyDirectory($this->outputDir); $this->createEmptyDirectory($this->dataDir); $this->createDirectoryFor($this->supportDir . DIRECTORY_SEPARATOR . '_generated'); $this->createDirectoryFor($this->supportDir . DIRECTORY_SEPARATOR . "Helper"); $this->gitIgnore('tests/_output'); $this->gitIgnore('tests/_support/_generated'); } protected function createFunctionalSuite($actor = 'Functional') { $suiteConfig = <<<EOF # Codeception Test Suite Configuration # # Suite for functional tests # Emulate web requests and make application process them # Include one of framework modules (Symfony2, Yii2, Laravel5) to use it # Remove this suite if you don't use frameworks actor: $actor{$this->actorSuffix} modules: enabled: # add a framework module here - \\{$this->namespace}Helper\Functional step_decorators: ~ EOF; $this->createSuite('functional', $actor, $suiteConfig); } protected function createAcceptanceSuite($actor = 'Acceptance') { $suiteConfig = <<<EOF # Codeception Test Suite Configuration # # Suite for acceptance tests. # Perform tests in browser using the WebDriver or PhpBrowser. # If you need both WebDriver and PHPBrowser tests - create a separate suite. actor: $actor{$this->actorSuffix} modules: enabled: - PhpBrowser: url: http://localhost/myapp - \\{$this->namespace}Helper\Acceptance step_decorators: ~ EOF; $this->createSuite('acceptance', $actor, $suiteConfig); } protected function createUnitSuite($actor = 'Unit') { $suiteConfig = <<<EOF # Codeception Test Suite Configuration # # Suite for unit or integration tests. actor: $actor{$this->actorSuffix} modules: enabled: - Asserts - \\{$this->namespace}Helper\Unit step_decorators: ~ EOF; $this->createSuite('unit', $actor, $suiteConfig); } public function createGlobalConfig() { $basicConfig = [ 'paths' => [ 'tests' => 'tests', 'output' => $this->outputDir, 'data' => $this->dataDir, 'support' => $this->supportDir, 'envs' => $this->envsDir, ], 'actor_suffix' => 'Tester', 'extensions' => [ 'enabled' => ['Codeception\Extension\RunFailed'] ] ]; $str = Yaml::dump($basicConfig, 4); if ($this->namespace) { $namespace = rtrim($this->namespace, '\\'); $str = "namespace: $namespace\n" . $str; } $this->createFile('codeception.yml', $str); } protected function createSuite($suite, $actor, $config) { $this->createDirectoryFor("tests/$suite", "$suite.suite.yml"); $this->createHelper($actor, $this->supportDir); $this->createActor($actor . $this->actorSuffix, $this->supportDir, Yaml::parse($config)); $this->createFile('tests' . DIRECTORY_SEPARATOR . "$suite.suite.yml", $config); } } <?php namespace Codeception\Template; use Codeception\InitTemplate; use Codeception\Util\Template; use Symfony\Component\Yaml\Yaml; class Unit extends InitTemplate { protected $configTemplate = <<<EOF suites: unit: path: . {{tester}} settings: shuffle: true lint: true paths: tests: {{dir}} output: {{dir}}/_output support: {{dir}}/_support data: {{dir}} EOF; protected $testerAndModules = <<<EOF actor: UnitTester modules: enabled: # add more modules here - Asserts step_decorators: ~ EOF; public function setup() { $this->sayInfo('This will install Codeception for unit testing only'); $this->say(); $dir = $this->ask("Where tests will be stored?", 'tests'); if (!$this->namespace) { $this->namespace = $this->ask("Enter a default namespace for tests (or skip this step)"); } $this->say(); $this->say("Codeception provides additional features for integration tests"); $this->say("Like accessing frameworks, ORM, Database."); $haveTester = $this->ask("Do you wish to enable them?", false); $this->createEmptyDirectory($outputDir = $dir . DIRECTORY_SEPARATOR . '_output'); $this->createEmptyDirectory($supportDir = $dir . DIRECTORY_SEPARATOR . '_support'); $configFile = (new Template($this->configTemplate)) ->place('dir', $dir) ->place('tester', $haveTester ? $this->testerAndModules : '') ->produce(); if ($this->namespace) { $namespace = rtrim($this->namespace, '\\'); $configFile = "namespace: $namespace\n" . $configFile; } $this->createFile('codeception.yml', $configFile); if ($haveTester) { $this->createHelper('Unit', $supportDir); $this->createActor('UnitTester', $supportDir, Yaml::parse($configFile)['suites']['unit']); } $this->gitIgnore($outputDir); $this->sayInfo("Created test directory inside at $dir"); $this->say(); $this->saySuccess("INSTALLATION COMPLETE"); $this->say(); $this->say('Unit tests will be executed in random order'); $this->say('Use @depends annotation to change the order of tests'); if ($haveTester) { $this->say('To access DI, ORM, Database enable corresponding modules in codeception.yml'); $this->say('Use <bold>$this->tester</bold> object inside Codeception\Test\Unit to call their methods'); $this->say("For example: \$this->tester->seeInDatabase('users', ['name' => 'davert'])"); } $this->say(); $this->say("<bold>Next steps:</bold>"); $this->say("Create the first test using <comment>codecept g:test unit MyTest</comment>"); $this->say("Run tests with <comment>codecept run</comment>"); $this->say("<bold>Happy testing!</bold>"); } } <?php namespace Codeception\Test; use Codeception\Exception\TestParseException; use Codeception\Lib\Parser; use Codeception\Lib\Console\Message; /** * Executes tests delivered in Cept format. * Prepares metadata, parses test body on preload, and executes a test in `test` method. */ class Cept extends Test implements Interfaces\Plain, Interfaces\ScenarioDriven, Interfaces\Reported, Interfaces\Dependent { use Feature\ScenarioLoader; /** * @var Parser */ protected $parser; public function __construct($name, $file) { $metadata = new Metadata(); $metadata->setName($name); $metadata->setFilename($file); $this->setMetadata($metadata); $this->createScenario(); $this->parser = new Parser($this->getScenario(), $this->getMetadata()); } public function preload() { $this->getParser()->prepareToRun($this->getSourceCode()); } public function test() { $scenario = $this->getScenario(); $testFile = $this->getMetadata()->getFilename(); /** @noinspection PhpIncludeInspection */ try { require $testFile; } catch (\ParseError $e) { throw new TestParseException($testFile, $e->getMessage(), $e->getLine()); } } public function getSignature() { return $this->getMetadata()->getName() . 'Cept'; } public function toString() { return $this->getSignature() . ': ' . Message::ucfirst($this->getFeature()); } public function getSourceCode() { return file_get_contents($this->getFileName()); } public function getReportFields() { return [ 'name' => basename($this->getFileName(), 'Cept.php'), 'file' => $this->getFileName(), 'feature' => $this->getFeature() ]; } /** * @return Parser */ protected function getParser() { return $this->parser; } public function fetchDependencies() { return $this->getMetadata()->getDependencies(); } } <?php namespace Codeception\Test; use Codeception\Example; use Codeception\Lib\Console\Message; use Codeception\Lib\Parser; use Codeception\Step\Comment; use Codeception\Util\Annotation; use Codeception\Util\ReflectionHelper; /** * Executes tests delivered in Cest format. * * Handles loading of Cest cases, executing specific methods, following the order from `@before` and `@after` annotations. */ class Cest extends Test implements Interfaces\ScenarioDriven, Interfaces\Reported, Interfaces\Dependent, Interfaces\StrictCoverage { use Feature\ScenarioLoader; /** * @var Parser */ protected $parser; protected $testClassInstance; protected $testMethod; public function __construct($testClass, $methodName, $fileName) { $metadata = new Metadata(); $metadata->setName($methodName); $metadata->setFilename($fileName); $this->setMetadata($metadata); $this->testClassInstance = $testClass; $this->testMethod = $methodName; $this->createScenario(); $this->parser = new Parser($this->getScenario(), $this->getMetadata()); } public function preload() { $this->scenario->setFeature($this->getSpecFromMethod()); $code = $this->getSourceCode(); $this->parser->parseFeature($code); $this->getMetadata()->setParamsFromAnnotations(Annotation::forMethod($this->testClassInstance, $this->testMethod)->raw()); $this->getMetadata()->getService('di')->injectDependencies($this->testClassInstance); // add example params to feature if ($this->getMetadata()->getCurrent('example')) { $step = new Comment('', $this->getMetadata()->getCurrent('example')); $this->getScenario()->setFeature($this->getScenario()->getFeature() . ' | '. $step->getArgumentsAsString(100)); } } public function getSourceCode() { $method = new \ReflectionMethod($this->testClassInstance, $this->testMethod); $start_line = $method->getStartLine() - 1; // it's actually - 1, otherwise you wont get the function() block $end_line = $method->getEndLine(); $source = file($method->getFileName()); return implode("", array_slice($source, $start_line, $end_line - $start_line)); } public function getSpecFromMethod() { $text = $this->testMethod; $text = preg_replace('/([A-Z]+)([A-Z][a-z])/', '\\1 \\2', $text); $text = preg_replace('/([a-z\d])([A-Z])/', '\\1 \\2', $text); $text = strtolower($text); return $text; } public function test() { $actorClass = $this->getMetadata()->getCurrent('actor'); $I = new $actorClass($this->getScenario()); try { $this->executeHook($I, 'before'); $this->executeBeforeMethods($this->testMethod, $I); $this->executeTestMethod($I); $this->executeAfterMethods($this->testMethod, $I); $this->executeHook($I, 'passed'); } catch (\Exception $e) { $this->executeHook($I, 'failed'); // fails and errors are now handled by Codeception\PHPUnit\Listener throw $e; } finally { $this->executeHook($I, 'after'); } } protected function executeHook($I, $hook) { if (is_callable([$this->testClassInstance, "_$hook"])) { $this->invoke("_$hook", [$I, $this->scenario]); } } protected function executeBeforeMethods($testMethod, $I) { $annotations = \PHPUnit\Util\Test::parseTestMethodAnnotations(get_class($this->testClassInstance), $testMethod); if (!empty($annotations['method']['before'])) { foreach ($annotations['method']['before'] as $m) { $this->executeContextMethod(trim($m), $I); } } } protected function executeAfterMethods($testMethod, $I) { $annotations = \PHPUnit\Util\Test::parseTestMethodAnnotations(get_class($this->testClassInstance), $testMethod); if (!empty($annotations['method']['after'])) { foreach ($annotations['method']['after'] as $m) { $this->executeContextMethod(trim($m), $I); } } } protected function executeContextMethod($context, $I) { if (method_exists($this->testClassInstance, $context)) { $this->executeBeforeMethods($context, $I); $this->invoke($context, [$I, $this->scenario]); $this->executeAfterMethods($context, $I); return; } throw new \LogicException( "Method $context defined in annotation but does not exist in " . get_class($this->testClassInstance) ); } protected function invoke($methodName, array $context) { foreach ($context as $class) { $this->getMetadata()->getService('di')->set($class); } $this->getMetadata()->getService('di')->injectDependencies($this->testClassInstance, $methodName, $context); } protected function executeTestMethod($I) { if (!method_exists($this->testClassInstance, $this->testMethod)) { throw new \Exception("Method {$this->testMethod} can't be found in tested class"); } if ($this->getMetadata()->getCurrent('example')) { $this->invoke($this->testMethod, [$I, $this->scenario, new Example($this->getMetadata()->getCurrent('example'))]); return; } $this->invoke($this->testMethod, [$I, $this->scenario]); } public function toString() { return sprintf('%s: %s', ReflectionHelper::getClassShortName($this->getTestClass()), Message::ucfirst($this->getFeature())); } public function getSignature() { return get_class($this->getTestClass()) . ":" . $this->getTestMethod(); } public function getTestClass() { return $this->testClassInstance; } public function getTestMethod() { return $this->testMethod; } /** * @return array */ public function getReportFields() { return [ 'file' => $this->getFileName(), 'name' => $this->getTestMethod(), 'class' => get_class($this->getTestClass()), 'feature' => $this->getFeature() ]; } protected function getParser() { return $this->parser; } public function fetchDependencies() { $names = []; foreach ($this->getMetadata()->getDependencies() as $required) { if ((strpos($required, ':') === false) and method_exists($this->getTestClass(), $required)) { $required = get_class($this->getTestClass()) . ":$required"; } $names[] = $required; } return $names; } public function getLinesToBeCovered() { $class = get_class($this->getTestClass()); $method = $this->getTestMethod(); return \PHPUnit\Util\Test::getLinesToBeCovered($class, $method); } public function getLinesToBeUsed() { $class = get_class($this->getTestClass()); $method = $this->getTestMethod(); return \PHPUnit\Util\Test::getLinesToBeUsed($class, $method); } } <?php namespace Codeception\Test; use Codeception\Test\Interfaces\Descriptive; use Codeception\Test\Interfaces\Plain; use Codeception\Util\ReflectionHelper; class Descriptor { /** * Provides a test name which can be located by * * @param \PHPUnit\Framework\SelfDescribing $testCase * @return string */ public static function getTestSignature(\PHPUnit\Framework\SelfDescribing $testCase) { if ($testCase instanceof Descriptive) { return $testCase->getSignature(); } if ($testCase instanceof \PHPUnit\Framework\TestCase) { return get_class($testCase) . ':' . $testCase->getName(false); } return $testCase->toString(); } /** * Provides a test name which is unique for individual iterations of tests using examples * * @param \PHPUnit\Framework\SelfDescribing $testCase * @return string */ public static function getTestSignatureUnique(\PHPUnit\Framework\SelfDescribing $testCase) { $env = ''; $example = ''; if (method_exists($testCase, 'getScenario') && !empty($testCase->getScenario()->current('env')) ) { $env = ':' . $testCase->getScenario()->current('env'); } if (method_exists($testCase, 'getMetaData') && !empty($testCase->getMetadata()->getCurrent('example')) ) { $example = ':' . substr(sha1(json_encode($testCase->getMetadata()->getCurrent('example'))), 0, 7); } return self::getTestSignature($testCase) . $env . $example; } public static function getTestAsString(\PHPUnit\Framework\SelfDescribing $testCase) { if ($testCase instanceof \PHPUnit\Framework\TestCase) { $text = $testCase->getName(); $text = preg_replace('/([A-Z]+)([A-Z][a-z])/', '\\1 \\2', $text); $text = preg_replace('/([a-z\d])([A-Z])/', '\\1 \\2', $text); $text = preg_replace('/^test /', '', $text); $text = ucfirst(strtolower($text)); $text = str_replace(['::', 'with data set'], [':', '|'], $text); return ReflectionHelper::getClassShortName($testCase) . ': ' . $text; } return $testCase->toString(); } /** * Provides a test file name relative to Codeception root * * @param \PHPUnit\Framework\SelfDescribing $testCase * @return mixed */ public static function getTestFileName(\PHPUnit\Framework\SelfDescribing $testCase) { if ($testCase instanceof Descriptive) { return codecept_relative_path(realpath($testCase->getFileName())); } return (new \ReflectionClass($testCase))->getFileName(); } /** * @param \PHPUnit\Framework\SelfDescribing $testCase * @return mixed|string */ public static function getTestFullName(\PHPUnit\Framework\SelfDescribing $testCase) { if ($testCase instanceof Plain) { return self::getTestFileName($testCase); } if ($testCase instanceof Descriptive) { $signature = $testCase->getSignature(); // cut everything before ":" from signature return self::getTestFileName($testCase) . ':' . preg_replace('~^(.*?):~', '', $signature); } if ($testCase instanceof \PHPUnit\Framework\TestCase) { return self::getTestFileName($testCase) . ':' . $testCase->getName(false); } return self::getTestFileName($testCase) . ':' . $testCase->toString(); } /** * Provides a test data set index * * @param \PHPUnit\Framework\SelfDescribing $testCase * @return int|null */ public static function getTestDataSetIndex(\PHPUnit\Framework\SelfDescribing $testCase) { if ($testCase instanceof Descriptive) { return $testCase->getMetadata()->getIndex(); } return null; } } <?php namespace Codeception\Test\Feature; trait AssertionCounter { protected $numAssertions = 0; public function getNumAssertions() { return $this->numAssertions; } protected function assertionCounterStart() { \PHPUnit\Framework\Assert::resetCount(); } protected function assertionCounterEnd() { $this->numAssertions = \PHPUnit\Framework\Assert::getCount(); } } <?php namespace Codeception\Test\Feature; use Codeception\Test\Descriptor; use Codeception\Test\Interfaces\StrictCoverage; trait CodeCoverage { /** * @return \PHPUnit\Framework\TestResult */ abstract public function getTestResultObject(); public function codeCoverageStart() { $codeCoverage = $this->getTestResultObject()->getCodeCoverage(); if (!$codeCoverage) { return; } $codeCoverage->start(Descriptor::getTestSignature($this)); } public function codeCoverageEnd($status, $time) { $codeCoverage = $this->getTestResultObject()->getCodeCoverage(); if (!$codeCoverage) { return; } if ($this instanceof StrictCoverage) { $linesToBeCovered = $this->getLinesToBeCovered(); $linesToBeUsed = $this->getLinesToBeUsed(); } else { $linesToBeCovered = []; $linesToBeUsed = []; } try { $codeCoverage->stop(true, $linesToBeCovered, $linesToBeUsed); } catch (\PHP_CodeCoverage_Exception $cce) { if ($status === \Codeception\Test\Test::STATUS_OK) { $this->getTestResultObject()->addError($this, $cce, $time); } } } } <?php namespace Codeception\Test\Feature; use Codeception\Test\Test as CodeceptionTest; trait ErrorLogger { /** * @return \PHPUnit\Framework\TestResult */ abstract public function getTestResultObject(); public function errorLoggerEnd($status, $time, $exception = null) { if (!$exception) { return; } if ($status === CodeceptionTest::STATUS_ERROR) { $this->getTestResultObject()->addError($this, $exception, $time); } if ($status === CodeceptionTest::STATUS_FAIL) { $this->getTestResultObject()->addFailure($this, $exception, $time); } } } <?php namespace Codeception\Test\Feature; use Codeception\Test\Metadata; trait IgnoreIfMetadataBlocked { /** * @return Metadata */ abstract protected function getMetadata(); abstract protected function ignore($ignored); /** * @return \PHPUnit\Framework\TestResult */ abstract protected function getTestResultObject(); protected function ignoreIfMetadataBlockedStart() { if (!$this->getMetadata()->isBlocked()) { return; } $this->ignore(true); if ($this->getMetadata()->getSkip() !== null) { $this->getTestResultObject()->addFailure($this, new \PHPUnit\Framework\SkippedTestError((string)$this->getMetadata()->getSkip()), 0); return; } if ($this->getMetadata()->getIncomplete() !== null) { $this->getTestResultObject()->addFailure($this, new \PHPUnit\Framework\IncompleteTestError((string)$this->getMetadata()->getIncomplete()), 0); return; } } } <?php namespace Codeception\Test\Feature; use Codeception\Test\Metadata; trait MetadataCollector { /** * @var Metadata */ private $metadata; protected function setMetadata(Metadata $metadata) { $this->metadata = $metadata; } public function getMetadata() { return $this->metadata; } public function getName() { return $this->getMetadata()->getName(); } public function getFileName() { return $this->getMetadata()->getFilename(); } } <?php namespace Codeception\Test\Feature; use Codeception\Lib\Parser; use Codeception\Scenario; use Codeception\Test\Metadata; trait ScenarioLoader { /** * @var Scenario */ private $scenario; /** * @return Metadata */ abstract public function getMetadata(); protected function createScenario() { $this->scenario = new Scenario($this); } /** * @return Scenario */ public function getScenario() { return $this->scenario; } public function getFeature() { return $this->getScenario()->getFeature(); } public function getScenarioText($format = 'text') { $code = $this->getSourceCode(); $this->getParser()->parseFeature($code); $this->getParser()->parseSteps($code); if ($format == 'html') { return $this->getScenario()->getHtml(); } return $this->getScenario()->getText(); } /** * @return Parser */ abstract protected function getParser(); abstract public function getSourceCode(); } <?php namespace Codeception\Test; use Behat\Gherkin\Node\FeatureNode; use Behat\Gherkin\Node\ScenarioNode; use Behat\Gherkin\Node\ScenarioInterface; use Behat\Gherkin\Node\StepNode; use Behat\Gherkin\Node\TableNode; use Codeception\Lib\Di; use Codeception\Lib\Generator\GherkinSnippets; use Codeception\Scenario; use Codeception\Step\Comment; use Codeception\Step\Meta; use Codeception\Test\Interfaces\Reported; use Codeception\Test\Interfaces\ScenarioDriven; class Gherkin extends Test implements ScenarioDriven, Reported { protected $steps = []; /** * @var FeatureNode */ protected $featureNode; /** * @var ScenarioNode */ protected $scenarioNode; /** * @var Scenario */ protected $scenario; public function __construct(FeatureNode $featureNode, ScenarioInterface $scenarioNode, $steps = []) { $this->featureNode = $featureNode; $this->scenarioNode = $scenarioNode; $this->steps = $steps; $this->setMetadata(new Metadata()); $this->scenario = new Scenario($this); $this->getMetadata()->setName($scenarioNode->getTitle()); $this->getMetadata()->setFeature($featureNode->getTitle()); $this->getMetadata()->setFilename($featureNode->getFile()); } public function preload() { $this->getMetadata()->setGroups($this->featureNode->getTags()); $this->getMetadata()->setGroups($this->scenarioNode->getTags()); $this->scenario->setMetaStep(null); if ($background = $this->featureNode->getBackground()) { foreach ($background->getSteps() as $step) { $this->validateStep($step); } } foreach ($this->scenarioNode->getSteps() as $step) { $this->validateStep($step); } if ($this->getMetadata()->getIncomplete()) { $this->getMetadata()->setIncomplete($this->getMetadata()->getIncomplete() . "\nRun gherkin:snippets to define missing steps"); } } public function getSignature() { return basename($this->getFileName(), '.feature') . ':' . $this->getScenarioTitle(); } public function test() { $this->makeContexts(); $description = explode("\n", $this->featureNode->getDescription()); foreach ($description as $line) { $this->getScenario()->runStep(new Comment($line)); } if ($background = $this->featureNode->getBackground()) { foreach ($background->getSteps() as $step) { $this->runStep($step); } } foreach ($this->scenarioNode->getSteps() as $step) { $this->runStep($step); } } protected function validateStep(StepNode $stepNode) { $stepText = $stepNode->getText(); if (GherkinSnippets::stepHasPyStringArgument($stepNode)) { $stepText .= ' ""'; } foreach ($this->steps as $pattern => $context) { $res = preg_match($pattern, $stepText); if (!$res) { continue; } return; } $incomplete = $this->getMetadata()->getIncomplete(); $this->getMetadata()->setIncomplete("$incomplete\nStep definition for `$stepText` not found in contexts"); } protected function runStep(StepNode $stepNode) { $params = []; if ($stepNode->hasArguments()) { $args = $stepNode->getArguments(); $table = $args[0]; if ($table instanceof TableNode) { $params = [$table->getTableAsString()]; } } $meta = new Meta($stepNode->getText(), $params); $meta->setPrefix($stepNode->getKeyword()); $this->scenario->setMetaStep($meta); // enable metastep $stepText = $stepNode->getText(); $hasPyStringArg = GherkinSnippets::stepHasPyStringArgument($stepNode); if ($hasPyStringArg) { // pretend it is inline argument $stepText .= ' ""'; } $this->getScenario()->comment(null); // make metastep to be printed even if no steps in it foreach ($this->steps as $pattern => $context) { $matches = []; if (!preg_match($pattern, $stepText, $matches)) { continue; } array_shift($matches); if ($hasPyStringArg) { // get rid off last fake argument array_pop($matches); } if ($stepNode->hasArguments()) { $matches = array_merge($matches, $stepNode->getArguments()); } call_user_func_array($context, $matches); // execute the step break; } $this->scenario->setMetaStep(null); // disable metastep } protected function makeContexts() { /** @var $di Di **/ $di = $this->getMetadata()->getService('di'); $di->set($this->getScenario()); $actorClass = $this->getMetadata()->getCurrent('actor'); if ($actorClass) { $di->set(new $actorClass($this->getScenario())); } foreach ($this->steps as $pattern => $step) { $di->instantiate($step[0]); $this->steps[$pattern][0] = $di->get($step[0]); } } public function toString() { return $this->getFeature() . ': ' . $this->getScenarioTitle(); } public function getFeature() { return $this->getMetadata()->getFeature(); } public function getScenarioTitle() { return $this->getMetadata()->getName(); } /** * @return \Codeception\Scenario */ public function getScenario() { return $this->scenario; } public function getScenarioText($format = 'text') { return file_get_contents($this->getFileName()); } public function getSourceCode() { } /** * @return ScenarioNode */ public function getScenarioNode() { return $this->scenarioNode; } /** * @return FeatureNode */ public function getFeatureNode() { return $this->featureNode; } /** * Field values for XML/JSON/TAP reports * * @return array */ public function getReportFields() { return [ 'file' => $this->getFileName(), 'name' => $this->toString(), 'feature' => $this->getFeature() ]; } } <?php namespace Codeception\Test\Interfaces; interface Dependent { public function fetchDependencies(); } <?php namespace Codeception\Test\Interfaces; interface Descriptive extends \PHPUnit\Framework\SelfDescribing { public function getFileName(); public function getSignature(); } <?php namespace Codeception\Test\Interfaces; /** * TestCases that do not follow OOP */ interface Plain { } <?php namespace Codeception\Test\Interfaces; interface Reported { /** * Field values for XML/JSON/TAP reports * * @return array */ public function getReportFields(); } <?php namespace Codeception\Test\Interfaces; interface ScenarioDriven { public function getFeature(); /** * @return \Codeception\Scenario */ public function getScenario(); public function getScenarioText($format = 'text'); public function preload(); public function getSourceCode(); } <?php namespace Codeception\Test\Interfaces; interface StrictCoverage { public function getLinesToBeCovered(); public function getLinesToBeUsed(); } <?php namespace Codeception\Test\Loader; use Codeception\Lib\Parser; use Codeception\Test\Cept as CeptFormat; class Cept implements LoaderInterface { protected $tests = []; public function getPattern() { return '~Cept\.php$~'; } function loadTests($file) { Parser::validate($file); $name = basename($file, 'Cept.php'); $cept = new CeptFormat($name, $file); $this->tests[] = $cept; } public function getTests() { return $this->tests; } } <?php namespace Codeception\Test\Loader; use Codeception\Exception\TestParseException; use Codeception\Lib\Parser; use Codeception\Test\Cest as CestFormat; use Codeception\Util\Annotation; use Codeception\Util\ReflectionHelper; class Cest implements LoaderInterface { protected $tests = []; public function getTests() { return $this->tests; } public function getPattern() { return '~Cest\.php$~'; } public function loadTests($file) { Parser::load($file); $testClasses = Parser::getClassesFromFile($file); foreach ($testClasses as $testClass) { if (substr($testClass, -strlen('Cest')) !== 'Cest') { continue; } if (!(new \ReflectionClass($testClass))->isInstantiable()) { continue; } $unit = new $testClass; $methods = get_class_methods($testClass); foreach ($methods as $method) { if (strpos($method, '_') === 0) { continue; } $examples = []; // example Annotation $rawExamples = Annotation::forMethod($unit, $method)->fetchAll('example'); if (count($rawExamples)) { $examples = array_map( function ($v) { return Annotation::arrayValue($v); }, $rawExamples ); } // dataProvider Annotation $dataMethod = Annotation::forMethod($testClass, $method)->fetch('dataProvider'); // lowercase for back compatible if (empty($dataMethod)) { $dataMethod = Annotation::forMethod($testClass, $method)->fetch('dataprovider'); } if (!empty($dataMethod)) { try { $data = ReflectionHelper::invokePrivateMethod($unit, $dataMethod); foreach ($data as $example) { $examples[] = $example; } } catch (\ReflectionException $e) { throw new TestParseException( $file, "DataProvider '$dataMethod' for $testClass->$method is invalid or not callable.\n" . "Make sure that the dataprovider exist within the test class." ); } } if (count($examples)) { $dataProvider = new \PHPUnit\Framework\DataProviderTestSuite(); $index = 0; foreach ($examples as $k => $example) { if ($example === null) { throw new TestParseException( $file, "Example for $testClass->$method contains invalid data:\n" . $rawExamples[$k] . "\n" . "Make sure this is a valid JSON (Hint: \"-char for strings) or a single-line annotation in Doctrine-style" ); } $test = new CestFormat($unit, $method, $file); $test->getMetadata()->setCurrent(['example' => $example]); $test->getMetadata()->setIndex($index); $dataProvider->addTest($test); $index++; } $this->tests[] = $dataProvider; continue; } $this->tests[] = new CestFormat($unit, $method, $file); } } } } <?php namespace Codeception\Test\Loader; use Behat\Gherkin\Filter\RoleFilter; use Behat\Gherkin\Keywords\ArrayKeywords as GherkinKeywords; use Behat\Gherkin\Lexer as GherkinLexer; use Behat\Gherkin\Node\ExampleNode; use Behat\Gherkin\Node\OutlineNode; use Behat\Gherkin\Node\ScenarioInterface; use Behat\Gherkin\Node\ScenarioNode; use Behat\Gherkin\Parser as GherkinParser; use Codeception\Configuration; use Codeception\Exception\ParseException; use Codeception\Exception\TestParseException; use Codeception\Test\Gherkin as GherkinFormat; use Codeception\Util\Annotation; class Gherkin implements LoaderInterface { protected static $defaultSettings = [ 'namespace' => '', 'actor' => '', 'gherkin' => [ 'contexts' => [ 'default' => [], 'tag' => [], 'role' => [] ] ] ]; protected $tests = []; /** * @var GherkinParser */ protected $parser; protected $settings = []; protected $steps = []; public function __construct($settings = []) { $this->settings = Configuration::mergeConfigs(self::$defaultSettings, $settings); if (!class_exists('Behat\Gherkin\Keywords\ArrayKeywords')) { throw new TestParseException('Feature file can only be parsed with Behat\Gherkin library. Please install `behat/gherkin` with Composer'); } $gherkin = new \ReflectionClass('Behat\Gherkin\Gherkin'); $gherkinClassPath = dirname($gherkin->getFileName()); $i18n = require $gherkinClassPath . '/../../../i18n.php'; $keywords = new GherkinKeywords($i18n); $lexer = new GherkinLexer($keywords); $this->parser = new GherkinParser($lexer); $this->fetchGherkinSteps(); } protected function fetchGherkinSteps() { $contexts = $this->settings['gherkin']['contexts']; foreach ($contexts['tag'] as $tag => $tagContexts) { $this->addSteps($tagContexts, "tag:$tag"); } foreach ($contexts['role'] as $role => $roleContexts) { $this->addSteps($roleContexts, "role:$role"); } if (empty($this->steps) && empty($contexts['default']) && $this->settings['actor']) { // if no context is set, actor to be a context $actorContext = $this->settings['namespace'] ? rtrim($this->settings['namespace'] . '\\' . $this->settings['actor'], '\\') : $this->settings['actor']; if ($actorContext) { $contexts['default'][] = $actorContext; } } if (isset($this->settings['gherkin']['contexts']['path']) && isset($this->settings['gherkin']['contexts']['namespace_prefix'])) { $files = glob($this->settings['gherkin']['contexts']['path'] . '/*/*.php'); // Strip off include path $files = str_replace([$this->settings['gherkin']['contexts']['path'], '.php', '/'], ['', '', '\\'], $files); // Add namespace prefix $namespace = $this->settings['gherkin']['contexts']['namespace_prefix']; $dynamicContexts = array_map(function ($path) use ($namespace) { return $namespace . $path; }, $files); $this->addSteps($dynamicContexts, 'default'); } $this->addSteps($contexts['default']); } protected function addSteps(array $contexts, $group = 'default') { if (!isset($this->steps[$group])) { $this->steps[$group] = []; } foreach ($contexts as $context) { $methods = get_class_methods($context); if (!$methods) { continue; } foreach ($methods as $method) { $annotation = Annotation::forMethod($context, $method); foreach (['Given', 'When', 'Then'] as $type) { $patterns = $annotation->fetchAll($type); foreach ($patterns as $pattern) { if (!$pattern) { continue; } $this->validatePattern($pattern); $pattern = $this->makePlaceholderPattern($pattern); $this->steps[$group][$pattern] = [$context, $method]; } } } } } public function makePlaceholderPattern($pattern) { if (isset($this->settings['describe_steps'])) { return $pattern; } if (strpos($pattern, '/') !== 0) { $pattern = preg_quote($pattern); $pattern = preg_replace('~(\w+)\/(\w+)~', '(?:$1|$2)', $pattern); // or $pattern = preg_replace('~\\\\\((\w)\\\\\)~', '$1?', $pattern); // (s) $replacePattern = sprintf( '(?|\"%s\"|%s)', "((?|[^\"\\\\\\]|\\\\\\.)*?)", // matching escaped string in "" '[\D]{0,1}([\d\,\.]+)[\D]{0,1}' ); // or matching numbers with optional $ or € chars // params converting from :param to match 11 and "aaa" and "aaa\"aaa" $pattern = preg_replace('~"?\\\:(\w+)"?~', $replacePattern, $pattern); $pattern = "/^$pattern$/u"; // validating this pattern is slow, so we skip it now } return $pattern; } private function validatePattern($pattern) { if (strpos($pattern, '/') !== 0) { return; // not a user-regex but a string with placeholder } if (@preg_match($pattern, ' ') === false) { throw new ParseException("Loading Gherkin step with regex\n \n$pattern\n \nfailed. This regular expression is invalid."); } } public function loadTests($filename) { $featureNode = $this->parser->parse(file_get_contents($filename), $filename); if (!$featureNode) { return; } foreach ($featureNode->getScenarios() as $scenarioNode) { /** @var $scenarioNode ScenarioInterface * */ $steps = $this->steps['default']; // load default context foreach (array_merge($scenarioNode->getTags(), $featureNode->getTags()) as $tag) { // load tag contexts if (isset($this->steps["tag:$tag"])) { $steps = array_merge($steps, $this->steps["tag:$tag"]); } } $roles = $this->settings['gherkin']['contexts']['role']; // load role contexts foreach ($roles as $role => $context) { $filter = new RoleFilter($role); if ($filter->isFeatureMatch($featureNode)) { $steps = array_merge($steps, $this->steps["role:$role"]); break; } } if ($scenarioNode instanceof OutlineNode) { foreach ($scenarioNode->getExamples() as $example) { /** @var $example ExampleNode **/ $params = implode(', ', $example->getTokens()); $exampleNode = new ScenarioNode($scenarioNode->getTitle() . " | $params", $scenarioNode->getTags(), $example->getSteps(), $example->getKeyword(), $example->getLine()); $this->tests[] = new GherkinFormat($featureNode, $exampleNode, $steps); } continue; } $this->tests[] = new GherkinFormat($featureNode, $scenarioNode, $steps); } } public function getTests() { return $this->tests; } public function getPattern() { return '~\.feature$~'; } /** * @return array */ public function getSteps() { return $this->steps; } } <?php namespace Codeception\Test\Loader; interface LoaderInterface { public function loadTests($filename); public function getTests(); public function getPattern(); } <?php namespace Codeception\Test\Loader; use Codeception\Lib\Parser; use Codeception\Test\Descriptor; use Codeception\Test\Unit as UnitFormat; use Codeception\Util\Annotation; class Unit implements LoaderInterface { protected $tests = []; public function getPattern() { return '~Test\.php$~'; } public function loadTests($path) { Parser::load($path); $testClasses = Parser::getClassesFromFile($path); foreach ($testClasses as $testClass) { $reflected = new \ReflectionClass($testClass); if (!$reflected->isInstantiable()) { continue; } foreach ($reflected->getMethods(\ReflectionMethod::IS_PUBLIC) as $method) { $test = $this->createTestFromPhpUnitMethod($reflected, $method); if (!$test) { continue; } $this->tests[] = $test; } } } public function getTests() { return $this->tests; } protected function createTestFromPhpUnitMethod(\ReflectionClass $class, \ReflectionMethod $method) { if (method_exists(\PHPUnit\Framework\TestSuite::class, 'isTestMethod')) { //PHPUnit <8.2 if (!\PHPUnit\Framework\TestSuite::isTestMethod($method)) { return; } $test = \PHPUnit\Framework\TestSuite::createTest($class, $method->name); } elseif (method_exists(\PHPUnit\Util\Test::class, 'isTestMethod')) { //PHPUnit >=8.2 if (!\PHPUnit\Util\Test::isTestMethod($method)) { return; } $test = (new \PHPUnit\Framework\TestBuilder)->build($class, $method->name); } else { throw new \Exception('Unsupported version of PHPUnit, where is isTestMethod method?'); } if ($test instanceof \PHPUnit\Framework\DataProviderTestSuite) { foreach ($test->tests() as $t) { $this->enhancePhpunitTest($t); } return $test; } $this->enhancePhpunitTest($test); return $test; } protected function enhancePhpunitTest(\PHPUnit\Framework\Test $test) { $className = get_class($test); $methodName = $test->getName(false); $dependencies = \PHPUnit\Util\Test::getDependencies($className, $methodName); $test->setDependencies($dependencies); if ($test instanceof UnitFormat) { $test->getMetadata()->setParamsFromAnnotations(Annotation::forMethod($test, $methodName)->raw()); $test->getMetadata()->setFilename(Descriptor::getTestFileName($test)); } } } <?php namespace Codeception\Test; use Codeception\Test\Loader\Cept as CeptLoader; use Codeception\Test\Loader\Cest as CestLoader; use Codeception\Test\Loader\Unit as UnitLoader; use Codeception\Test\Loader\Gherkin as GherkinLoader; use Symfony\Component\Finder\Finder; /** * Loads all Codeception supported test formats from a directory. * * ``` php * <?php * $testLoader = new \Codeception\TestLoader('tests/unit'); * $testLoader->loadTests(); * $tests = $testLoader->getTests(); * ?> * ``` * You can load specific file * * ``` php * <?php * $testLoader = new \Codeception\TestLoader('tests/unit'); * $testLoader->loadTest('UserTest.php'); * $testLoader->loadTest('PostTest.php'); * $tests = $testLoader->getTests(); * ?> * ``` * or a subdirectory * * ``` php * <?php * $testLoader = new \Codeception\TestLoader('tests/unit'); * $testLoader->loadTest('models'); // all tests from tests/unit/models * $tests = $testLoader->getTests(); * ?> * ``` * */ class Loader { protected $formats = []; protected $tests = []; protected $path; public function __construct(array $suiteSettings) { $this->path = $suiteSettings['path']; $this->formats = [ new CeptLoader(), new CestLoader(), new UnitLoader(), new GherkinLoader($suiteSettings) ]; if (isset($suiteSettings['formats'])) { foreach ($suiteSettings['formats'] as $format) { $this->formats[] = new $format($suiteSettings); } } } public function getTests() { return $this->tests; } protected function relativeName($file) { return str_replace([$this->path, '\\'], ['', '/'], $file); } protected function findPath($path) { if (!file_exists($path) && substr($path, -strlen('.php')) !== '.php' && file_exists($newPath = $path . '.php') ) { return $newPath; } return $path; } protected function makePath($originalPath) { $path = $this->path . $this->relativeName($originalPath); if (file_exists($newPath = $this->findPath($path)) || file_exists($newPath = $this->findPath(getcwd() . "/{$originalPath}")) ) { $path = $newPath; } if (!file_exists($path)) { throw new \Exception("File or path $originalPath not found"); } return $path; } public function loadTest($path) { $path = $this->makePath($path); foreach ($this->formats as $format) { /** @var $format Loader **/ if (preg_match($format->getPattern(), $path)) { $format->loadTests($path); $this->tests = $format->getTests(); return; } } if (is_dir($path)) { $currentPath = $this->path; $this->path = $path; $this->loadTests(); $this->path = $currentPath; return; } throw new \Exception('Test format not supported. Please, check you use the right suffix. Available filetypes: Cept, Cest, Test'); } public function loadTests($fileName = null) { if ($fileName) { return $this->loadTest($fileName); } $finder = Finder::create()->files()->sortByName()->in($this->path)->followLinks(); foreach ($this->formats as $format) { /** @var $format Loader **/ $formatFinder = clone($finder); $testFiles = $formatFinder->name($format->getPattern()); foreach ($testFiles as $test) { $pathname = str_replace(["//", "\\\\"], ["/", "\\"], $test->getPathname()); $format->loadTests($pathname); } $this->tests = array_merge($this->tests, $format->getTests()); } } } <?php namespace Codeception\Test; use Codeception\Exception\InjectionException; use Codeception\Util\Annotation; class Metadata { protected $name; protected $filename; protected $feature; protected $index; protected $params = [ 'env' => [], 'group' => [], 'depends' => [], 'skip' => null, 'incomplete' => null ]; protected $current = []; protected $services = []; protected $reports = []; /** * @return mixed */ public function getEnv() { return $this->params['env']; } /** * @return array */ public function getGroups() { return array_unique($this->params['group']); } /** * @param mixed $groups */ public function setGroups($groups) { $this->params['group'] = array_merge($this->params['group'], $groups); } /** * @return mixed */ public function getSkip() { return $this->params['skip']; } /** * @param mixed $skip */ public function setSkip($skip) { $this->params['skip'] = $skip; } /** * @return mixed */ public function getIncomplete() { return $this->params['incomplete']; } /** * @param mixed $incomplete */ public function setIncomplete($incomplete) { $this->params['incomplete'] = $incomplete; } /** * @param string|null $key * @return mixed */ public function getCurrent($key = null) { if ($key) { if (isset($this->current[$key])) { return $this->current[$key]; } if ($key === 'name') { return $this->getName(); } return null; } return $this->current; } public function setCurrent(array $currents) { $this->current = array_merge($this->current, $currents); } /** * @return mixed */ public function getName() { return $this->name; } /** * @param mixed $name */ public function setName($name) { $this->name = $name; } /** * @return mixed */ public function getFilename() { return $this->filename; } /** * @param mixed $index */ public function setIndex($index) { $this->index = $index; } /** * @return mixed */ public function getIndex() { return $this->index; } /** * @param mixed $filename */ public function setFilename($filename) { $this->filename = $filename; } /** * @return array */ public function getDependencies() { return $this->params['depends']; } public function isBlocked() { return $this->getSkip() !== null || $this->getIncomplete() !== null; } /** * @return mixed */ public function getFeature() { return $this->feature; } /** * @param mixed $feature */ public function setFeature($feature) { $this->feature = $feature; } /** * @param $service * @return array * @throws InjectionException */ public function getService($service) { if (!isset($this->services[$service])) { throw new InjectionException("Service $service is not defined and can't be accessed from a test"); } return $this->services[$service]; } /** * @param array $services */ public function setServices($services) { $this->services = $services; } /** * Returns all test reports * @return array */ public function getReports() { return $this->reports; } /** * @param $type * @param $report */ public function addReport($type, $report) { $this->reports[$type] = $report; } /** * Returns test params like: env, group, skip, incomplete, etc * Can return by annotation or return all if no key passed * * @param null $key * @return array|mixed|null */ public function getParam($key = null) { if ($key) { if (isset($this->params[$key])) { return $this->params[$key]; } return null; } return $this->params; } /** * @param mixed $annotations */ public function setParamsFromAnnotations($annotations) { $params = Annotation::fetchAllAnnotationsFromDocblock($annotations); $this->params = array_merge_recursive($this->params, $params); // set singular value for some params foreach (['skip', 'incomplete'] as $single) { $this->params[$single] = empty($this->params[$single]) ? null : (string) $this->params[$single][0]; } } /** * @param $params */ public function setParams($params) { $this->params = array_merge_recursive($this->params, $params); } } <?php namespace Codeception\Test; use Codeception\TestInterface; use Codeception\Util\ReflectionHelper; use SebastianBergmann\Timer\Timer; /** * The most simple testcase (with only one test in it) which can be executed by PHPUnit/Codeception. * It can be extended with included traits. Turning on/off a trait should not break class functionality. * * Class has exactly one method to be executed for testing, wrapped with before/after callbacks delivered from included traits. * A trait providing before/after callback should contain corresponding protected methods: `{traitName}Start` and `{traitName}End`, * then this trait should be enabled in `hooks` property. * * Inherited class must implement `test` method. */ abstract class Test implements TestInterface, Interfaces\Descriptive { use Feature\AssertionCounter; use Feature\CodeCoverage; use Feature\ErrorLogger; use Feature\MetadataCollector; use Feature\IgnoreIfMetadataBlocked; private $testResult; private $ignored = false; /** * Enabled traits with methods to be called before and after the test. * * @var array */ protected $hooks = [ 'ignoreIfMetadataBlocked', 'codeCoverage', 'assertionCounter', 'errorLogger' ]; const STATUS_FAIL = 'fail'; const STATUS_ERROR = 'error'; const STATUS_OK = 'ok'; const STATUS_PENDING = 'pending'; /** * Everything inside this method is treated as a test. * * @return mixed */ abstract public function test(); /** * Test representation * * @return mixed */ abstract public function toString(); /** * Runs a test and collects its result in a TestResult instance. * Executes before/after hooks coming from traits. * * @param \PHPUnit\Framework\TestResult $result * @return \PHPUnit\Framework\TestResult */ final public function run(\PHPUnit\Framework\TestResult $result = null) { $this->testResult = $result; $status = self::STATUS_PENDING; $time = 0; $e = null; $result->startTest($this); foreach ($this->hooks as $hook) { if (method_exists($this, $hook.'Start')) { $this->{$hook.'Start'}(); } } $failedToStart = ReflectionHelper::readPrivateProperty($result, 'lastTestFailed'); if (!$this->ignored && !$failedToStart) { Timer::start(); try { $this->test(); $status = self::STATUS_OK; } catch (\PHPUnit\Framework\AssertionFailedError $e) { $status = self::STATUS_FAIL; } catch (\PHPUnit\Framework\Exception $e) { $status = self::STATUS_ERROR; } catch (\Throwable $e) { $e = new \PHPUnit\Framework\ExceptionWrapper($e); $status = self::STATUS_ERROR; } catch (\Exception $e) { $e = new \PHPUnit\Framework\ExceptionWrapper($e); $status = self::STATUS_ERROR; } $time = Timer::stop(); } foreach (array_reverse($this->hooks) as $hook) { if (method_exists($this, $hook.'End')) { $this->{$hook.'End'}($status, $time, $e); } } $result->endTest($this, $time); return $result; } public function getTestResultObject() { return $this->testResult; } /** * This class represents exactly one test * @return int */ public function count() { return 1; } /** * Should a test be skipped (can be set from hooks) * * @param boolean $ignored */ protected function ignore($ignored) { $this->ignored = $ignored; } } <?php namespace Codeception\Test; use Codeception\Configuration; use Codeception\Exception\ModuleException; use Codeception\Lib\Di; use Codeception\Lib\Notification; use Codeception\Scenario; use Codeception\TestInterface; /** * Represents tests from PHPUnit compatible format. */ class Unit extends \Codeception\PHPUnit\TestCase implements Interfaces\Reported, Interfaces\Dependent, TestInterface { use \Codeception\Test\Feature\Stub; /** * @var Metadata */ private $metadata; public function getMetadata() { if (!$this->metadata) { $this->metadata = new Metadata(); } return $this->metadata; } protected function _setUp() { if ($this->getMetadata()->isBlocked()) { if ($this->getMetadata()->getSkip() !== null) { $this->markTestSkipped($this->getMetadata()->getSkip()); } if ($this->getMetadata()->getIncomplete() !== null) { $this->markTestIncomplete($this->getMetadata()->getIncomplete()); } return; } /** @var $di Di **/ $di = $this->getMetadata()->getService('di'); $di->set(new Scenario($this)); // auto-inject $tester property if (($this->getMetadata()->getCurrent('actor')) && ($property = lcfirst(Configuration::config()['actor_suffix']))) { $this->$property = $di->instantiate($this->getMetadata()->getCurrent('actor')); } // Auto inject into the _inject method $di->injectDependencies($this); // injecting dependencies $this->_before(); } /** * @Override */ protected function _before() { } protected function _tearDown() { $this->_after(); } /** * @Override */ protected function _after() { } /** * @param $module * @return \Codeception\Module * @throws ModuleException */ public function getModule($module) { $modules = $this->getMetadata()->getCurrent('modules'); if (!isset($modules[$module])) { throw new ModuleException($module, "Module can't be accessed"); } return $modules[$module]; } /** * Returns current values */ public function getCurrent($current) { return $this->getMetadata()->getCurrent($current); } /** * @return array */ public function getReportFields() { return [ 'name' => $this->getName(), 'class' => get_class($this), 'file' => $this->getMetadata()->getFilename() ]; } public function fetchDependencies() { $names = []; foreach ($this->getMetadata()->getDependencies() as $required) { if ((strpos($required, ':') === false) and method_exists($this, $required)) { $required = get_class($this) . ":$required"; } $names[] = $required; } return $names; } /** * Reset PHPUnit's dependencies * @return bool */ public function handleDependencies() { $dependencies = $this->fetchDependencies(); if (empty($dependencies)) { return true; } $passed = $this->getTestResultObject()->passed(); $dependencyInput = []; foreach ($dependencies as $dependency) { $dependency = str_replace(':', '::', $dependency); // Codeception => PHPUnit format if (strpos($dependency, '::') === false) { // check it is method of same class $dependency = get_class($this) . '::' . $dependency; } if (isset($passed[$dependency])) { $dependencyInput[] = $passed[$dependency]['result']; } else { $dependencyInput[] = null; } } $this->setDependencyInput($dependencyInput); return true; } } <?php namespace Codeception; use Codeception\Test\Metadata; interface TestInterface extends \PHPUnit\Framework\Test { /** * @return Metadata */ public function getMetadata(); } <?php namespace Codeception\Util; use Codeception\Step\Action; /** * Class for defining an array actions to be executed inside `performOn` of WebDriver * * ```php * <?php * (new ActionSequence)->click('do')->click('undo'); * ActionSequence::build()->click('do')->click('undo'); * ``` * * @method $this see([optional]) * @method $this dontSee([optional]) * @method $this seeElement([optional]) * @method $this dontSeeElement([optional]) * @method $this click([optional]) * @method $this wait([optional]) * @method $this waitForElementChange([optional]) * @method $this waitForElement([optional]) * @method $this waitForElementVisible([optional]) * @method $this waitForElementNotVisible([optional]) * @method $this waitForText([optional]) * @method $this submitForm([optional]) * @method $this seeLink([optional]) * @method $this dontSeeLink([optional]) * @method $this seeCheckboxIsChecked([optional]) * @method $this dontSeeCheckboxIsChecked([optional]) * @method $this seeInField([optional]) * @method $this dontSeeInField([optional]) * @method $this seeInFormFields([optional]) * @method $this dontSeeInFormFields([optional]) * @method $this selectOption([optional]) * @method $this checkOption([optional]) * @method $this uncheckOption([optional]) * @method $this fillField([optional]) * @method $this attachFile([optional]) * @method $this seeNumberOfElements([optional]) * @method $this seeOptionIsSelected([optional]) * @method $this dontSeeOptionIsSelected([optional]) */ class ActionSequence { protected $actions = []; /** * Creates an instance * @return ActionSequence */ public static function build() { return new self; } public function __call($action, $arguments) { $this->addAction($action, $arguments); return $this; } protected function addAction($action, $arguments) { if (!is_array($arguments)) { $arguments = [$arguments]; } $this->actions[] = new Action($action, $arguments); } /** * Creates action sequence from associative array, * where key is action, and value is action arguments * * @param array $actions * @return $this */ public function fromArray(array $actions) { foreach ($actions as $action => $arguments) { $this->addAction($action, $arguments); } return $this; } /** * Returns a list of logged actions as associative array * @return array */ public function toArray() { return $this->actions; } /** * Executes sequence of action as methods of passed object. * * @param $context */ public function run($context) { foreach ($this->actions as $step) { /** @var $step Action **/ codecept_debug("- $step"); try { call_user_func_array([$context, $step->getAction()], $step->getArguments()); } catch (\Exception $e) { $class = get_class($e); // rethrow exception for a specific action throw new $class($e->getMessage() . "\nat $step"); } } } public function __toString() { $actionsLog = []; foreach ($this->actions as $step) { $args = str_replace('"', "'", $step->getArgumentsAsString(20)); $actionsLog[] = $step->getAction() . ": $args"; } return implode(', ', $actionsLog); } } <?php namespace Codeception\Util; /** * Simple annotation parser. Take only key-value annotations for methods or class. */ class Annotation { protected static $reflectedClasses = []; protected static $regex = '/@%s(?:[ \t]*(.*?))?[ \t]*\r?$/m'; protected static $lastReflected = null; /** * @var \ReflectionClass */ protected $reflectedClass; protected $currentReflectedItem; /** * Grabs annotation values. * * Usage example: * * ``` php * <?php * Annotation::forClass('MyTestCase')->fetch('guy'); * Annotation::forClass('MyTestCase')->method('testData')->fetch('depends'); * Annotation::forClass('MyTestCase')->method('testData')->fetchAll('depends'); * * ?> * ``` * * @param $class * * @return $this */ public static function forClass($class) { if (is_object($class)) { $class = get_class($class); } if (!isset(static::$reflectedClasses[$class])) { static::$reflectedClasses[$class] = new \ReflectionClass($class); } return new static(static::$reflectedClasses[$class]); } /** * @param $class * @param $method * * @return $this */ public static function forMethod($class, $method) { return self::forClass($class)->method($method); } /** * Parses raw comment for annotations * * @param $docblock * @param $annotation * @return array */ public static function fetchAnnotationsFromDocblock($annotation, $docblock) { if (preg_match_all(sprintf(self::$regex, $annotation), $docblock, $matched)) { return $matched[1]; } return []; } /** * Fetches all available annotations * * @param $docblock * @return array */ public static function fetchAllAnnotationsFromDocblock($docblock) { $annotations = []; if (!preg_match_all(sprintf(self::$regex, '(\w+)'), $docblock, $matched)) { return $annotations; } foreach ($matched[1] as $k => $annotation) { if (!isset($annotations[$annotation])) { $annotations[$annotation] = []; } $annotations[$annotation][] = $matched[2][$k]; }; return $annotations; } public function __construct(\ReflectionClass $class) { $this->currentReflectedItem = $this->reflectedClass = $class; } /** * @param $method * * @return $this */ public function method($method) { $this->currentReflectedItem = $this->reflectedClass->getMethod($method); return $this; } /** * @param $annotation * @return null */ public function fetch($annotation) { $docBlock = $this->currentReflectedItem->getDocComment(); if (preg_match(sprintf(self::$regex, $annotation), $docBlock, $matched)) { return $matched[1]; } return null; } /** * @param $annotation * @return array */ public function fetchAll($annotation) { $docBlock = $this->currentReflectedItem->getDocComment(); if (preg_match_all(sprintf(self::$regex, $annotation), $docBlock, $matched)) { return $matched[1]; } return []; } public function raw() { return $this->currentReflectedItem->getDocComment(); } /** * Returns an associative array value of annotation * Either JSON or Doctrine-annotation style allowed * Returns null if not a valid array data * * @param $annotation * @return array|mixed|string */ public static function arrayValue($annotation) { $annotation = trim($annotation); $openingBrace = substr($annotation, 0, 1); // json-style data format if (in_array($openingBrace, ['{', '['])) { return json_decode($annotation, true); } // doctrine-style data format if ($openingBrace === '(') { preg_match_all('~(\w+)\s*?=\s*?"(.*?)"\s*?[,)]~', $annotation, $matches, PREG_SET_ORDER); $data = []; foreach ($matches as $item) { $data[$item[1]] = $item[2]; } return $data; } return null; } } <?php namespace Codeception\Util; class ArrayContainsComparator { /** * @var array */ protected $haystack = []; public function __construct($haystack) { $this->haystack = $haystack; } /** * @return array */ public function getHaystack() { return $this->haystack; } public function containsArray(array $needle) { return $needle == $this->arrayIntersectRecursive($needle, $this->haystack); } /** * @author nleippe@integr8ted.com * @author tiger.seo@gmail.com * @link http://www.php.net/manual/en/function.array-intersect-assoc.php#39822 * * @param mixed $arr1 * @param mixed $arr2 * * @return array|bool */ private function arrayIntersectRecursive($arr1, $arr2) { if (!is_array($arr1) || !is_array($arr2)) { return false; } // if it is not an associative array we do not compare keys if ($this->arrayIsSequential($arr1) && $this->arrayIsSequential($arr2)) { return $this->sequentialArrayIntersect($arr1, $arr2); } return $this->associativeArrayIntersect($arr1, $arr2); } /** * This array has sequential keys? * * @param array $array * * @return bool */ private function arrayIsSequential(array $array) { return array_keys($array) === range(0, count($array) - 1); } /** * @param array $arr1 * @param array $arr2 * @return array */ private function sequentialArrayIntersect(array $arr1, array $arr2) { $ret = []; // Do not match the same item of $arr2 against multiple items of $arr1 $matchedKeys = []; foreach ($arr1 as $key1 => $value1) { foreach ($arr2 as $key2 => $value2) { if (isset($matchedKeys[$key2])) { continue; } $return = $this->arrayIntersectRecursive($value1, $value2); if ($return !== false && $return == $value1) { $ret[$key1] = $return; $matchedKeys[$key2] = true; break; } if ($this->isEqualValue($value1, $value2)) { $ret[$key1] = $value1; $matchedKeys[$key2] = true; break; } } } return $ret; } /** * @param array $arr1 * @param array $arr2 * * @return array|bool|null */ private function associativeArrayIntersect(array $arr1, array $arr2) { $commonKeys = array_intersect(array_keys($arr1), array_keys($arr2)); $ret = []; foreach ($commonKeys as $key) { $return = $this->arrayIntersectRecursive($arr1[$key], $arr2[$key]); if ($return !== false) { $ret[$key] = $return; continue; } if ($this->isEqualValue($arr1[$key], $arr2[$key])) { $ret[$key] = $arr1[$key]; } } if (empty($commonKeys)) { foreach ($arr2 as $arr) { $return = $this->arrayIntersectRecursive($arr1, $arr); if ($return && $return == $arr1) { return $return; } } } if (count($ret) < min(count($arr1), count($arr2))) { return null; } return $ret; } private function isEqualValue($val1, $val2) { if (is_numeric($val1)) { $val1 = (string) $val1; } if (is_numeric($val2)) { $val2 = (string) $val2; } return $val1 === $val2; } } <?php namespace Codeception\Util; /** * Autoloader, which is fully compatible with PSR-4, * and can be used to autoload your `Helper`, `Page`, and `Step` classes. */ class Autoload { protected static $registered = false; /** * An associative array where the key is a namespace prefix and the value * is an array of base directories for classes in that namespace. * @var array */ protected static $map = []; private function __construct() { } /** * Adds a base directory for a namespace prefix. * * Example: * * ```php * <?php * // app\Codeception\UserHelper will be loaded from '/path/to/helpers/UserHelper.php' * Autoload::addNamespace('app\Codeception', '/path/to/helpers'); * * // LoginPage will be loaded from '/path/to/pageobjects/LoginPage.php' * Autoload::addNamespace('', '/path/to/pageobjects'); * * Autoload::addNamespace('app\Codeception', '/path/to/controllers'); * ?> * ``` * * @param string $prefix The namespace prefix. * @param string $base_dir A base directory for class files in the namespace. * @param bool $prepend If true, prepend the base directory to the stack instead of appending it; * this causes it to be searched first rather than last. * @return void */ public static function addNamespace($prefix, $base_dir, $prepend = false) { if (!self::$registered) { spl_autoload_register([__CLASS__, 'load']); self::$registered = true; } // normalize namespace prefix $prefix = trim($prefix, '\\') . '\\'; // normalize the base directory with a trailing separator $base_dir = rtrim($base_dir, '/') . DIRECTORY_SEPARATOR; $base_dir = rtrim($base_dir, DIRECTORY_SEPARATOR) . '/'; // initialize the namespace prefix array if (isset(self::$map[$prefix]) === false) { self::$map[$prefix] = []; } // retain the base directory for the namespace prefix if ($prepend) { array_unshift(self::$map[$prefix], $base_dir); } else { array_push(self::$map[$prefix], $base_dir); } } public static function load($class) { // the current namespace prefix $prefix = $class; // work backwards through the namespace names of the fully-qualified class name to find a mapped file name while (false !== ($pos = strrpos($prefix, '\\'))) { // retain the trailing namespace separator in the prefix $prefix = substr($class, 0, $pos + 1); // the rest is the relative class name $relative_class = substr($class, $pos + 1); // try to load a mapped file for the prefix and relative class $mapped_file = self::loadMappedFile($prefix, $relative_class); if ($mapped_file) { return $mapped_file; } // remove the trailing namespace separator for the next iteration of strrpos() $prefix = rtrim($prefix, '\\'); } // fix for empty prefix if (isset(self::$map['\\']) && ($class[0] != '\\')) { return self::load('\\' . $class); } // backwards compatibility with old autoloader // :TODO: it should be removed if (strpos($class, '\\') !== false) { $relative_class = substr(strrchr($class, '\\'), 1); // Foo\Bar\ClassName -> ClassName $mapped_file = self::loadMappedFile('\\', $relative_class); if ($mapped_file) { return $mapped_file; } } return false; } /** * Load the mapped file for a namespace prefix and relative class. * * @param string $prefix The namespace prefix. * @param string $relative_class The relative class name. * @return mixed Boolean false if no mapped file can be loaded, or the name of the mapped file that was loaded. */ protected static function loadMappedFile($prefix, $relative_class) { if (!isset(self::$map[$prefix])) { return false; } foreach (self::$map[$prefix] as $base_dir) { $file = $base_dir . str_replace('\\', '/', $relative_class) . '.php'; // 'static' is for testing purposes if (static::requireFile($file)) { return $file; } } return false; } protected static function requireFile($file) { if (file_exists($file)) { require_once $file; return true; } return false; } } <?php namespace Codeception\Util; use Codeception\Lib\Console\Output; use Symfony\Component\Console\Helper\QuestionHelper; use Symfony\Component\Console\Input\ArgvInput; use Symfony\Component\Console\Question\ConfirmationQuestion; use Symfony\Component\Console\Question\Question; /** * This class is used only when Codeception is executed in `--debug` mode. * In other cases method of this class won't be seen. */ class Debug { /** * @var Output null */ protected static $output = null; public static function setOutput(Output $output) { self::$output = $output; } /** * Prints data to screen. Message can be any time of data * * @param $message */ public static function debug($message) { if (!self::$output) { return; } self::$output->debug($message); } public static function isEnabled() { return (bool) self::$output; } public static function confirm($question) { if (!self::$output) { return; } $questionHelper = new QuestionHelper(); return $questionHelper->ask(new ArgvInput(), self::$output, new ConfirmationQuestion($question)); } } <?php namespace Codeception\Util; /** * Set of functions to work with Filesystem * */ class FileSystem { /** * @param $path */ public static function doEmptyDir($path) { /** @var $iterator \RecursiveIteratorIterator|\SplFileObject[] */ $iterator = new \RecursiveIteratorIterator( new \RecursiveDirectoryIterator($path), \RecursiveIteratorIterator::CHILD_FIRST ); foreach ($iterator as $path) { $basename = basename((string)$path); if ($basename === '.' || $basename === '..' || $basename === '.gitignore') { continue; } if ($path->isDir()) { rmdir((string)$path); } else { unlink((string)$path); } } } /** * @param $dir * @return bool */ public static function deleteDir($dir) { if (!file_exists($dir)) { return true; } if (!is_dir($dir) || is_link($dir)) { return @unlink($dir); } if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') { $dir = str_replace('/', '\\', $dir); exec('rd /s /q "'.$dir.'"'); return true; } foreach (scandir($dir) as $item) { if ($item === '.' || $item === '..') { continue; } if (!self::deleteDir($dir . DIRECTORY_SEPARATOR . $item)) { chmod($dir . DIRECTORY_SEPARATOR . $item, 0777); if (!self::deleteDir($dir . DIRECTORY_SEPARATOR . $item)) { return false; } } } return @rmdir($dir); } /** * @param $src * @param $dst */ public static function copyDir($src, $dst) { $dir = opendir($src); @mkdir($dst); while (false !== ($file = readdir($dir))) { if (($file != '.') && ($file != '..')) { if (is_dir($src . DIRECTORY_SEPARATOR . $file)) { self::copyDir($src . DIRECTORY_SEPARATOR . $file, $dst . DIRECTORY_SEPARATOR . $file); } else { copy($src . DIRECTORY_SEPARATOR . $file, $dst . DIRECTORY_SEPARATOR . $file); } } } closedir($dir); } } <?php namespace Codeception\Util; /** * Really basic class to store data in global array and use it in Cests/Tests. * * ```php * <?php * Fixtures::add('user1', ['name' => 'davert']); * Fixtures::get('user1'); * Fixtures::exists('user1'); * * ?> * ``` * */ class Fixtures { protected static $fixtures = []; public static function add($name, $data) { self::$fixtures[$name] = $data; } public static function get($name) { if (!self::exists($name)) { throw new \RuntimeException("$name not found in fixtures"); } return self::$fixtures[$name]; } public static function cleanup() { self::$fixtures = []; } public static function exists($name) { return isset(self::$fixtures[$name]); } } <?php namespace Codeception\Util; /** * Class containing constants of HTTP Status Codes * and method to print HTTP code with its description. * * Usage: * * ```php * <?php * use \Codeception\Util\HttpCode; * * // using REST, PhpBrowser, or any Framework module * $I->seeResponseCodeIs(HttpCode::OK); * $I->dontSeeResponseCodeIs(HttpCode::NOT_FOUND); * ``` * * */ class HttpCode { const SWITCHING_PROTOCOLS = 101; const PROCESSING = 102; // RFC2518 const OK = 200; const CREATED = 201; const ACCEPTED = 202; const NON_AUTHORITATIVE_INFORMATION = 203; const NO_CONTENT = 204; const RESET_CONTENT = 205; const PARTIAL_CONTENT = 206; const MULTI_STATUS = 207; // RFC4918 const ALREADY_REPORTED = 208; // RFC5842 const IM_USED = 226; // RFC3229 const MULTIPLE_CHOICES = 300; const MOVED_PERMANENTLY = 301; const FOUND = 302; const SEE_OTHER = 303; const NOT_MODIFIED = 304; const USE_PROXY = 305; const RESERVED = 306; const TEMPORARY_REDIRECT = 307; const PERMANENTLY_REDIRECT = 308; // RFC7238 const BAD_REQUEST = 400; const UNAUTHORIZED = 401; const PAYMENT_REQUIRED = 402; const FORBIDDEN = 403; const NOT_FOUND = 404; const METHOD_NOT_ALLOWED = 405; const NOT_ACCEPTABLE = 406; const PROXY_AUTHENTICATION_REQUIRED = 407; const REQUEST_TIMEOUT = 408; const CONFLICT = 409; const GONE = 410; const LENGTH_REQUIRED = 411; const PRECONDITION_FAILED = 412; const REQUEST_ENTITY_TOO_LARGE = 413; const REQUEST_URI_TOO_LONG = 414; const UNSUPPORTED_MEDIA_TYPE = 415; const REQUESTED_RANGE_NOT_SATISFIABLE = 416; const EXPECTATION_FAILED = 417; const I_AM_A_TEAPOT = 418; // RFC2324 const UNPROCESSABLE_ENTITY = 422; // RFC4918 const LOCKED = 423; // RFC4918 const FAILED_DEPENDENCY = 424; // RFC4918 const RESERVED_FOR_WEBDAV_ADVANCED_COLLECTIONS_EXPIRED_PROPOSAL = 425; // RFC2817 const UPGRADE_REQUIRED = 426; // RFC2817 const PRECONDITION_REQUIRED = 428; // RFC6585 const TOO_MANY_REQUESTS = 429; // RFC6585 const REQUEST_HEADER_FIELDS_TOO_LARGE = 431; // RFC6585 const INTERNAL_SERVER_ERROR = 500; const NOT_IMPLEMENTED = 501; const BAD_GATEWAY = 502; const SERVICE_UNAVAILABLE = 503; const GATEWAY_TIMEOUT = 504; const VERSION_NOT_SUPPORTED = 505; const VARIANT_ALSO_NEGOTIATES_EXPERIMENTAL = 506; // RFC2295 const INSUFFICIENT_STORAGE = 507; // RFC4918 const LOOP_DETECTED = 508; // RFC5842 const NOT_EXTENDED = 510; // RFC2774 const NETWORK_AUTHENTICATION_REQUIRED = 511; // RFC6585 private static $codes = [ 100 => 'Continue', 102 => 'Processing', 200 => 'OK', 201 => 'Created', 202 => 'Accepted', 203 => 'Non-Authoritative Information', 204 => 'No Content', 205 => 'Reset Content', 206 => 'Partial Content', 207 => 'Multi-Status', 208 => 'Already Reported', 226 => 'IM Used', 300 => 'Multiple Choices', 301 => 'Moved Permanently', 302 => 'Found', 303 => 'See Other', 304 => 'Not Modified', 305 => 'Use Proxy', 307 => 'Temporary Redirect', 308 => 'Permanent Redirect', 400 => 'Bad Request', 401 => 'Unauthorized', 402 => 'Payment Required', 403 => 'Forbidden', 404 => 'Not Found', 405 => 'Method Not Allowed', 406 => 'Not Acceptable', 407 => 'Proxy Authentication Required', 408 => 'Request Timeout', 409 => 'Conflict', 410 => 'Gone', 411 => 'Length Required', 412 => 'Precondition Failed', 413 => 'Request Entity Too Large', 414 => 'Request-URI Too Long', 415 => 'Unsupported Media Type', 416 => 'Requested Range Not Satisfiable', 417 => 'Expectation Failed', 421 => 'Misdirected Request', 422 => 'Unprocessable Entity', 423 => 'Locked', 424 => 'Failed Dependency', 426 => 'Upgrade Required', 428 => 'Precondition Required', 429 => 'Too Many Requests', 431 => 'Request Header Fields Too Large', 500 => 'Internal Server Error', 501 => 'Not Implemented', 502 => 'Bad Gateway', 503 => 'Service Unavailable', 504 => 'Gateway Timeout', 505 => 'HTTP Version Not Supported', 506 => 'Variant Also Negotiates', 507 => 'Insufficient Storage', 508 => 'Loop Detected', 510 => 'Not Extended', 511 => 'Network Authentication Required' ]; /** * Returns string with HTTP code and its description * * ```php * <?php * HttpCode::getDescription(200); // '200 (OK)' * HttpCode::getDescription(401); // '401 (Unauthorized)' * ``` * * @param $code * @return mixed */ public static function getDescription($code) { if (isset(self::$codes[$code])) { return sprintf('%d (%s)', $code, self::$codes[$code]); } return $code; } } <?php namespace Codeception\Util; use Flow\JSONPath\JSONPath; use InvalidArgumentException; use DOMDocument; class JsonArray { /** * @var array */ protected $jsonArray = []; /** * @var DOMDocument */ protected $jsonXml = null; public function __construct($jsonString) { if (!is_string($jsonString)) { throw new InvalidArgumentException('$jsonString param must be a string.'); } $jsonDecode = json_decode($jsonString, true); if (!is_array($jsonDecode)) { $jsonDecode = [$jsonDecode]; } $this->jsonArray = $jsonDecode; if (JSON_ERROR_NONE !== json_last_error()) { throw new InvalidArgumentException( sprintf( "Invalid json: %s. System message: %s.", $jsonString, json_last_error_msg() ), json_last_error() ); } } public function toXml() { if ($this->jsonXml) { return $this->jsonXml; } $root = 'root'; $jsonArray = $this->jsonArray; if (count($jsonArray) == 1) { $value = reset($jsonArray); if (is_array($value)) { $root = key($jsonArray); $jsonArray = $value; } } $dom = new DOMDocument('1.0', 'UTF-8'); $dom->formatOutput = false; $root = $dom->createElement($root); $dom->appendChild($root); $this->arrayToXml($dom, $root, $jsonArray); $this->jsonXml = $dom; return $dom; } /** * @return array */ public function toArray() { return $this->jsonArray; } public function filterByXPath($xpath) { $path = new \DOMXPath($this->toXml()); return $path->query($xpath); } public function filterByJsonPath($jsonPath) { if (!class_exists('Flow\JSONPath\JSONPath')) { throw new \Exception('JSONPath library not installed. Please add `flow/jsonpath` to composer.json'); } return (new JSONPath($this->jsonArray))->find($jsonPath)->data(); } public function getXmlString() { return $this->toXml()->saveXML(); } public function containsArray(array $needle) { return (new ArrayContainsComparator($this->jsonArray))->containsArray($needle); } private function arrayToXml(\DOMDocument $doc, \DOMNode $node, $array) { foreach ($array as $key => $value) { if (is_numeric($key)) { $subNode = $doc->createElement($node->nodeName); $node->parentNode->appendChild($subNode); } else { try { $subNode = $doc->createElement($key); } catch (\Exception $e) { $key = $this->getValidTagNameForInvalidKey($key); $subNode = $doc->createElement($key); } $node->appendChild($subNode); } if (is_array($value)) { $this->arrayToXml($doc, $subNode, $value); } else { $subNode->nodeValue = htmlspecialchars((string)$value); } } } private function getValidTagNameForInvalidKey($key) { static $map = []; if (!isset($map[$key])) { $tagName = 'invalidTag' . (count($map) + 1); $map[$key] = $tagName; codecept_debug($tagName . ' is "' . $key . '"'); } return $map[$key]; } } <?php namespace Codeception\Util; /** * JsonType matches JSON structures against templates. * You can specify the type of fields in JSON or add additional validation rules. * * JsonType is used by REST module in `seeResponseMatchesJsonType` and `dontSeeResponseMatchesJsonType` methods. * * Usage example: * * ```php * <?php * $jsonType = new JsonType(['name' => 'davert', 'id' => 1]); * $jsonType->matches([ * 'name' => 'string:!empty', * 'id' => 'integer:>0|string:>0', * ]); // => true * * $jsonType->matches([ * 'id' => 'string', * ]); // => `id: 1` is not of type string * ?> * ``` * * Class JsonType * @package Codeception\Util */ class JsonType { protected $jsonArray; protected static $customFilters = []; /** * Creates instance of JsonType * Pass an array or `\Codeception\Util\JsonArray` with data. * If non-associative array is passed - the very first element of it will be used for matching. * * @param $jsonArray array|\Codeception\Util\JsonArray */ public function __construct($jsonArray) { if ($jsonArray instanceof JsonArray) { $jsonArray = $jsonArray->toArray(); } $this->jsonArray = $jsonArray; } /** * Adds custom filter to JsonType list. * You should specify a name and parameters of a filter. * * Example: * * ```php * <?php * JsonType::addCustomFilter('slug', function($value) { * return strpos(' ', $value) !== false; * }); * // => use it as 'string:slug' * * // add custom function to matcher with `len($val)` syntax * // parameter matching patterns should be valid regex and start with `/` char * JsonType::addCustomFilter('/len\((.*?)\)/', function($value, $len) { * return strlen($value) == $len; * }); * // use it as 'string:len(5)' * ?> * ``` * * @param $name * @param callable $callable */ public static function addCustomFilter($name, callable $callable) { static::$customFilters[$name] = $callable; } /** * Removes all custom filters */ public static function cleanCustomFilters() { static::$customFilters = []; } /** * Checks data against passed JsonType. * If matching fails function returns a string with a message describing failure. * On success returns `true`. * * @param array $jsonType * @return bool|string */ public function matches(array $jsonType) { if (array_key_exists(0, $this->jsonArray) && is_array($this->jsonArray[0])) { // a list of items $msg = ''; foreach ($this->jsonArray as $array) { $res = $this->typeComparison($array, $jsonType); if ($res !== true) { $msg .= "\n" . $res; } } if ($msg) { return $msg; } return true; } return $this->typeComparison($this->jsonArray, $jsonType); } protected function typeComparison($data, $jsonType) { foreach ($jsonType as $key => $type) { if (!array_key_exists($key, $data)) { return "Key `$key` doesn't exist in " . json_encode($data); } if (is_array($jsonType[$key])) { $message = $this->typeComparison($data[$key], $jsonType[$key]); if (is_string($message)) { return $message; } continue; } $regexMatcher = '/:regex\((((\()|(\{)|(\[)|(<)|(.)).*?(?(3)\)|(?(4)\}|(?(5)\]|(?(6)>|\7)))))\)/'; $regexes = []; // Match the string ':regex(' and any characters until a ending regex delimiter followed by character ')' // Place the 'any character' + delimiter matches in to an array. preg_match_all($regexMatcher, $type, $regexes); // Do the same match as above, but replace the the 'any character' + delimiter with a place holder ($${count}). $filterType = preg_replace_callback($regexMatcher, function () { static $count = 0; return ':regex($$' . $count++ . ')'; }, $type); $matchTypes = preg_split("#(?![^]\(]*\))\|#", $filterType); $matched = false; $currentType = strtolower(gettype($data[$key])); if ($currentType === 'double') { $currentType = 'float'; } foreach ($matchTypes as $matchType) { $filters = preg_split("#(?![^]\(]*\))\:#", $matchType); $expectedType = strtolower(trim(array_shift($filters))); if ($expectedType !== $currentType) { continue; } $matched = true; foreach ($filters as $filter) { // Fill regex pattern back into the filter. $filter = preg_replace_callback('/\$\$\d+/', function ($m) use ($regexes) { $pos = (int)substr($m[0], 2); return $regexes[1][$pos]; }, $filter); $matched = $matched && $this->matchFilter($filter, $data[$key]); } if ($matched) { break; } } if (!$matched) { return sprintf("`$key: %s` is of type `$type`", var_export($data[$key], true)); } } return true; } protected function matchFilter($filter, $value) { $filter = trim($filter); if (strpos($filter, '!') === 0) { return !$this->matchFilter(substr($filter, 1), $value); } // apply custom filters foreach (static::$customFilters as $customFilter => $callable) { if (strpos($customFilter, '/') === 0) { if (preg_match($customFilter, $filter, $matches)) { array_shift($matches); return call_user_func_array($callable, array_merge([$value], $matches)); } } if ($customFilter == $filter) { return $callable($value); } } if (strpos($filter, '=') === 0) { return $value == substr($filter, 1); } if ($filter === 'url') { return filter_var($value, FILTER_VALIDATE_URL); } if ($filter === 'date') { return preg_match( '/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(\.\d+)?(?:Z|(\+|-)([\d|:]*))?$/', $value ); } if ($filter === 'email') { // from http://emailregex.com/ // @codingStandardsIgnoreStart return preg_match('/^(?!(?:(?:\x22?\x5C[\x00-\x7E]\x22?)|(?:\x22?[^\x5C\x22]\x22?)){255,})(?!(?:(?:\x22?\x5C[\x00-\x7E]\x22?)|(?:\x22?[^\x5C\x22]\x22?)){65,}@)(?:(?:[\x21\x23-\x27\x2A\x2B\x2D\x2F-\x39\x3D\x3F\x5E-\x7E]+)|(?:\x22(?:[\x01-\x08\x0B\x0C\x0E-\x1F\x21\x23-\x5B\x5D-\x7F]|(?:\x5C[\x00-\x7F]))*\x22))(?:\.(?:(?:[\x21\x23-\x27\x2A\x2B\x2D\x2F-\x39\x3D\x3F\x5E-\x7E]+)|(?:\x22(?:[\x01-\x08\x0B\x0C\x0E-\x1F\x21\x23-\x5B\x5D-\x7F]|(?:\x5C[\x00-\x7F]))*\x22)))*@(?:(?:(?!.*[^.]{64,})(?:(?:(?:xn--)?[a-z0-9]+(?:-[a-z0-9]+)*\.){1,126}){1,}(?:(?:[a-z][a-z0-9]*)|(?:(?:xn--)[a-z0-9]+))(?:-[a-z0-9]+)*)|(?:\[(?:(?:IPv6:(?:(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){7})|(?:(?!(?:.*[a-f0-9][:\]]){7,})(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){0,5})?::(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){0,5})?)))|(?:(?:IPv6:(?:(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){5}:)|(?:(?!(?:.*[a-f0-9]:){5,})(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){0,3})?::(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){0,3}:)?)))?(?:(?:25[0-5])|(?:2[0-4][0-9])|(?:1[0-9]{2})|(?:[1-9]?[0-9]))(?:\.(?:(?:25[0-5])|(?:2[0-4][0-9])|(?:1[0-9]{2})|(?:[1-9]?[0-9]))){3}))\]))$/iD', $value); // @codingStandardsIgnoreEnd } if ($filter === 'empty') { return empty($value); } if (preg_match('~^regex\((.*?)\)$~', $filter, $matches)) { return preg_match($matches[1], $value); } if (preg_match('~^>([\d\.]+)$~', $filter, $matches)) { return (float)$value > (float)$matches[1]; } if (preg_match('~^<([\d\.]+)$~', $filter, $matches)) { return (float)$value < (float)$matches[1]; } } } <?php namespace Codeception\Util; use Facebook\WebDriver\WebDriverBy; use Symfony\Component\CssSelector\CssSelectorConverter; use Symfony\Component\CssSelector\Exception\ParseException; use Symfony\Component\CssSelector\XPath\Translator; /** * Set of useful functions for using CSS and XPath locators. * Please check them before writing complex functional or acceptance tests. * */ class Locator { /** * Applies OR operator to any number of CSS or XPath selectors. * You can mix up CSS and XPath selectors here. * * ```php * <?php * use \Codeception\Util\Locator; * * $I->see('Title', Locator::combine('h1','h2','h3')); * ?> * ``` * * This will search for `Title` text in either `h1`, `h2`, or `h3` tag. * You can also combine CSS selector with XPath locator: * * ```php * <?php * use \Codeception\Util\Locator; * * $I->fillField(Locator::combine('form input[type=text]','//form/textarea[2]'), 'qwerty'); * ?> * ``` * * As a result the Locator will produce a mixed XPath value that will be used in fillField action. * * @static * * @param $selector1 * @param $selector2 * * @throws \Exception * * @return string */ public static function combine($selector1, $selector2) { $selectors = func_get_args(); foreach ($selectors as $k => $v) { $selectors[$k] = self::toXPath($v); if (!$selectors[$k]) { throw new \Exception("$v is invalid CSS or XPath"); } } return implode(' | ', $selectors); } /** * Matches the *a* element with given URL * * ```php * <?php * use \Codeception\Util\Locator; * * $I->see('Log In', Locator::href('/login.php')); * ?> * ``` * * @static * * @param $url * * @return string */ public static function href($url) { return sprintf('//a[@href=normalize-space(%s)]', Translator::getXpathLiteral($url)); } /** * Matches the element with given tab index * * Do you often use the `TAB` key to navigate through the web page? How do your site respond to this navigation? * You could try to match elements by their tab position using `tabIndex` method of `Locator` class. * ```php * <?php * use \Codeception\Util\Locator; * * $I->fillField(Locator::tabIndex(1), 'davert'); * $I->fillField(Locator::tabIndex(2) , 'qwerty'); * $I->click('Login'); * ?> * ``` * * @static * * @param $index * * @return string */ public static function tabIndex($index) { return sprintf('//*[@tabindex = normalize-space(%d)]', $index); } /** * Matches option by text: * * ```php * <?php * use Codeception\Util\Locator; * * $I->seeElement(Locator::option('Male'), '#select-gender'); * ``` * * @param $value * * @return string */ public static function option($value) { return sprintf('//option[.=normalize-space("%s")]', $value); } protected static function toXPath($selector) { try { $xpath = (new CssSelectorConverter())->toXPath($selector); return $xpath; } catch (ParseException $e) { if (self::isXPath($selector)) { return $selector; } } return null; } /** * Finds element by it's attribute(s) * * ```php * <?php * use \Codeception\Util\Locator; * * $I->seeElement(Locator::find('img', ['title' => 'diagram'])); * ``` * * @static * * @param $element * @param $attributes * * @return string */ public static function find($element, array $attributes) { $operands = []; foreach ($attributes as $attribute => $value) { if (is_int($attribute)) { $operands[] = '@' . $value; } else { $operands[] = '@' . $attribute . ' = ' . Translator::getXpathLiteral($value); } } return sprintf('//%s[%s]', $element, implode(' and ', $operands)); } /** * Checks that provided string is CSS selector * * ```php * <?php * Locator::isCSS('#user .hello') => true * Locator::isCSS('body') => true * Locator::isCSS('//body/p/user') => false * ``` * * @param $selector * * @return bool */ public static function isCSS($selector) { try { (new CssSelectorConverter())->toXPath($selector); } catch (ParseException $e) { return false; } return true; } /** * Checks that locator is an XPath * * ```php * <?php * Locator::isXPath('#user .hello') => false * Locator::isXPath('body') => false * Locator::isXPath('//body/p/user') => true * ``` * * @param $locator * * @return bool */ public static function isXPath($locator) { $document = new \DOMDocument('1.0', 'UTF-8'); $xpath = new \DOMXPath($document); return @$xpath->evaluate($locator, $document) !== false; } /** * @param $locator * @return bool */ public static function isPrecise($locator) { if (is_array($locator)) { return true; } if ($locator instanceof WebDriverBy) { return true; } if (Locator::isID($locator)) { return true; } if (strpos($locator, '//') === 0) { return true; // simple xpath check } return false; } /** * Checks that a string is valid CSS ID * * ```php * <?php * Locator::isID('#user') => true * Locator::isID('body') => false * Locator::isID('//body/p/user') => false * ``` * * @param $id * * @return bool */ public static function isID($id) { return (bool)preg_match('~^#[\w\.\-\[\]\=\^\~\:]+$~', $id); } /** * Checks that a string is valid CSS class * * ```php * <?php * Locator::isClass('.hello') => true * Locator::isClass('body') => false * Locator::isClass('//body/p/user') => false * ``` * * @param $class * @return bool */ public static function isClass($class) { return (bool)preg_match('~^\.[\w\.\-\[\]\=\^\~\:]+$~', $class); } /** * Locates an element containing a text inside. * Either CSS or XPath locator can be passed, however they will be converted to XPath. * * ```php * <?php * use Codeception\Util\Locator; * * Locator::contains('label', 'Name'); // label containing name * Locator::contains('div[@contenteditable=true]', 'hello world'); * ``` * * @param $element * @param $text * * @return string */ public static function contains($element, $text) { $text = Translator::getXpathLiteral($text); return sprintf('%s[%s]', self::toXPath($element), "contains(., $text)"); } /** * Locates element at position. * Either CSS or XPath locator can be passed as locator, * position is an integer. If a negative value is provided, counting starts from the last element. * First element has index 1 * * ```php * <?php * use Codeception\Util\Locator; * * Locator::elementAt('//table/tr', 2); // second row * Locator::elementAt('//table/tr', -1); // last row * Locator::elementAt('table#grind>tr', -2); // previous than last row * ``` * * @param string $element CSS or XPath locator * @param int $position xpath index * * @return mixed */ public static function elementAt($element, $position) { if (is_int($position) && $position < 0) { $position++; // -1 points to the last element $position = 'last()-'.abs($position); } if ($position === 0) { throw new \InvalidArgumentException( '0 is not valid element position. XPath expects first element to have index 1' ); } return sprintf('(%s)[position()=%s]', self::toXPath($element), $position); } /** * Locates first element of group elements. * Either CSS or XPath locator can be passed as locator, * Equal to `Locator::elementAt($locator, 1)` * * ```php * <?php * use Codeception\Util\Locator; * * Locator::firstElement('//table/tr'); * ``` * * @param $element * * @return mixed */ public static function firstElement($element) { return self::elementAt($element, 1); } /** * Locates last element of group elements. * Either CSS or XPath locator can be passed as locator, * Equal to `Locator::elementAt($locator, -1)` * * ```php * <?php * use Codeception\Util\Locator; * * Locator::lastElement('//table/tr'); * ``` * * @param $element * * @return mixed */ public static function lastElement($element) { return self::elementAt($element, 'last()'); } /** * Transforms strict locator, \Facebook\WebDriver\WebDriverBy into a string represenation * * @param $selector * * @return string */ public static function humanReadableString($selector) { if (is_string($selector)) { return "'$selector'"; } if (is_array($selector)) { $type = strtolower(key($selector)); $locator = $selector[$type]; return "$type '$locator'"; } if (class_exists('\Facebook\WebDriver\WebDriverBy')) { if ($selector instanceof WebDriverBy) { $type = $selector->getMechanism(); $locator = $selector->getValue(); return "$type '$locator'"; } } throw new \InvalidArgumentException("Unrecognized selector"); } } <?php namespace Codeception\Util; /** * Class to represent any type of content. * This class can act as an object, array, or string. * Method or property calls to this class won't cause any errors. * * Maybe was used in Codeception 1.x to represent data on parsing step. * Not widely used in 2.0 anymore, but left for compatibility. * * For instance, you may use `Codeception\Util\Maybe` as a test dummies. * * ```php * <?php * $user = new Maybe; * $user->posts->comments->count(); * ?> * ``` */ class Maybe implements \ArrayAccess, \Iterator, \JsonSerializable { protected $position = 0; protected $val = null; protected $assocArray = null; public function __construct($val = null) { $this->val = $val; if (is_array($this->val)) { $this->assocArray = $this->isAssocArray($this->val); } $this->position = 0; } private function isAssocArray($arr) { return array_keys($arr) !== range(0, count($arr) - 1); } public function __toString() { if ($this->val === null) { return "?"; } if (is_scalar($this->val)) { return (string)$this->val; } if (is_object($this->val) && method_exists($this->val, '__toString')) { return $this->val->__toString(); } return $this->val; } public function __get($key) { if ($this->val === null) { return new Maybe(); } if (is_object($this->val)) { if (isset($this->val->{$key}) || property_exists($this->val, $key)) { return $this->val->{$key}; } } return $this->val->key; } public function __set($key, $val) { if ($this->val === null) { return; } if (is_object($this->val)) { $this->val->{$key} = $val; return; } $this->val->key = $val; } public function __call($method, $args) { if ($this->val === null) { return new Maybe(); } return call_user_func_array([$this->val, $method], $args); } public function __clone() { if (is_object($this->val)) { $this->val = clone $this->val; } } public function __unset($key) { if (is_object($this->val)) { if (isset($this->val->{$key}) || property_exists($this->val, $key)) { unset($this->val->{$key}); return; } } } public function offsetExists($offset) { if (is_array($this->val) || ($this->val instanceof \ArrayAccess)) { return isset($this->val[$offset]); } return false; } public function offsetGet($offset) { if (is_array($this->val) || ($this->val instanceof \ArrayAccess)) { return $this->val[$offset]; } return new Maybe(); } public function offsetSet($offset, $value) { if (is_array($this->val) || ($this->val instanceof \ArrayAccess)) { $this->val[$offset] = $value; } } public function offsetUnset($offset) { if (is_array($this->val) || ($this->val instanceof \ArrayAccess)) { unset($this->val[$offset]); } } public function __value() { $val = $this->val; if (is_array($val)) { foreach ($val as $k => $v) { if ($v instanceof self) { $v = $v->__value(); } $val[$k] = $v; } } return $val; } /** * (PHP 5 >= 5.0.0)<br/> * Return the current element * @link http://php.net/manual/en/iterator.current.php * @return mixed Can return any type. */ public function current() { if (!is_array($this->val)) { return null; } if ($this->assocArray) { $keys = array_keys($this->val); return $this->val[$keys[$this->position]]; } return $this->val[$this->position]; } /** * (PHP 5 >= 5.0.0)<br/> * Move forward to next element * @link http://php.net/manual/en/iterator.next.php * @return void Any returned value is ignored. */ public function next() { ++$this->position; } /** * (PHP 5 >= 5.0.0)<br/> * Return the key of the current element * @link http://php.net/manual/en/iterator.key.php * @return mixed scalar on success, or null on failure. */ public function key() { if ($this->assocArray) { $keys = array_keys($this->val); return $keys[$this->position]; } return $this->position; } /** * (PHP 5 >= 5.0.0)<br/> * Checks if current position is valid * @link http://php.net/manual/en/iterator.valid.php * @return boolean The return value will be casted to boolean and then evaluated. * Returns true on success or false on failure. */ public function valid() { if (!is_array($this->val)) { return null; } if ($this->assocArray) { $keys = array_keys($this->val); return isset($keys[$this->position]); } return isset($this->val[$this->position]); } /** * (PHP 5 >= 5.0.0)<br/> * Rewind the Iterator to the first element * @link http://php.net/manual/en/iterator.rewind.php * @return void Any returned value is ignored. */ public function rewind() { if (is_array($this->val)) { $this->assocArray = $this->isAssocArray($this->val); } $this->position = 0; } /** * (PHP 5 >= 5.4.0) * Serializes the object to a value that can be serialized natively by json_encode(). * @link http://docs.php.net/manual/en/jsonserializable.jsonserialize.php * @return mixed Returns data which can be serialized by json_encode(), which is a value of any type other than a resource. */ public function jsonSerialize() { return $this->__value(); } } <?php namespace Codeception\Util; use Codeception\Exception\ConfigurationException; class PathResolver { /** * Returns path to a given directory relative to $projDir. * @param string $path * @param string $projDir * @param string $dirSep * @return string */ public static function getRelativeDir($path, $projDir, $dirSep = DIRECTORY_SEPARATOR) { // ensure $projDir ends with a trailing $dirSep $projDir = preg_replace('/'.preg_quote($dirSep, '/').'*$/', $dirSep, $projDir); // if $path is a within $projDir if (self::fsCaseStrCmp(substr($path, 0, strlen($projDir)), $projDir, $dirSep) == 0) { // simply chop it off the front return substr($path, strlen($projDir)); } // Identify any absoluteness prefix (like '/' in Unix or "C:\\" in Windows) $pathAbsPrefix = self::getPathAbsolutenessPrefix($path, $dirSep); $projDirAbsPrefix = self::getPathAbsolutenessPrefix($projDir, $dirSep); $sameAbsoluteness = (self::fsCaseStrCmp($pathAbsPrefix['wholePrefix'], $projDirAbsPrefix['wholePrefix'], $dirSep) == 0); if (!$sameAbsoluteness) { // if the $projDir and $path aren't relative to the same // thing, we can't make a relative path. // if we're relative to the same device ... if (strlen($pathAbsPrefix['devicePrefix']) && (self::fsCaseStrCmp($pathAbsPrefix['devicePrefix'], $projDirAbsPrefix['devicePrefix'], $dirSep) == 0) ) { // ... shave that off return substr($path, strlen($pathAbsPrefix['devicePrefix'])); } // Return the input unaltered return $path; } // peel off optional absoluteness prefixes and convert // $path and $projDir to an subdirectory path array $relPathParts = array_filter(explode($dirSep, substr($path, strlen($pathAbsPrefix['wholePrefix']))), 'strlen'); $relProjDirParts = array_filter(explode($dirSep, substr($projDir, strlen($projDirAbsPrefix['wholePrefix']))), 'strlen'); // While there are any, peel off any common parent directories // from the beginning of the $projDir and $path while ((count($relPathParts) > 0) && (count($relProjDirParts) > 0) && (self::fsCaseStrCmp($relPathParts[0], $relProjDirParts[0], $dirSep) == 0) ) { array_shift($relPathParts); array_shift($relProjDirParts); } if (count($relProjDirParts) > 0) { // prefix $relPath with '..' for all remaining unmatched $projDir // subdirectories $relPathParts = array_merge(array_fill(0, count($relProjDirParts), '..'), $relPathParts); } // only append a trailing seperator if one is already present $trailingSep = preg_match('/'.preg_quote($dirSep, '/').'$/', $path) ? $dirSep : ''; // convert array of dir paths back into a string path return implode($dirSep, $relPathParts).$trailingSep; } /** * FileSystem Case String Compare * compare two strings with the filesystem's case-sensitiveness * * @param string $str1 * @param string $str2 * @param string $dirSep * @return int -1 / 0 / 1 for < / = / > respectively */ private static function fsCaseStrCmp($str1, $str2, $dirSep = DIRECTORY_SEPARATOR) { $cmpFn = self::isWindows($dirSep) ? 'strcasecmp' : 'strcmp'; return $cmpFn($str1, $str2); } /** * What part of this path (leftmost 0-3 characters) what * it is absolute relative to: * * On Unix: * This is simply '/' for an absolute path or * '' for a relative path * * On Windows this is more complicated: * If the first two characters are a letter followed * by a ':', this indicates that the path is * on a specific device. * With or without a device specified, a path MAY * start with a '\\' to indicate an absolute path * on the device or '' to indicate a path relative * to the device's CWD * * @param string $path * @param string $dirSep * @return string */ private static function getPathAbsolutenessPrefix($path, $dirSep = DIRECTORY_SEPARATOR) { $devLetterPrefixPattern = ''; if (self::isWindows($dirSep)) { $devLetterPrefixPattern = '([A-Za-z]:|)'; } $matches = []; if (!preg_match('/^'.$devLetterPrefixPattern.preg_quote($dirSep, '/').'?/', $path, $matches)) { // This should match, even if it matches 0 characters throw new ConfigurationException("INTERNAL ERROR: This must be a regex problem."); } return [ 'wholePrefix' => $matches[0], // The optional device letter followed by the optional $dirSep 'devicePrefix' => self::isWindows($dirSep) ? $matches[1] : '']; } /** * Are we in a Windows style filesystem? * * @param string $dirSep * @return bool */ private static function isWindows($dirSep = DIRECTORY_SEPARATOR) { return ($dirSep == '\\'); } } <?php namespace Codeception\Util; /** * This class contains helper methods to help with common Reflection tasks. */ class ReflectionHelper { /** * Read a private property of an object. * * @param object $object * @param string $property * @param string|null $class * @return mixed */ public static function readPrivateProperty($object, $property, $class = null) { if (is_null($class)) { $class = $object; } $property = new \ReflectionProperty($class, $property); $property->setAccessible(true); return $property->getValue($object); } /** * Invoke a private method of an object. * * @param object $object * @param string $method * @param array $args * @param string|null $class * @return mixed */ public static function invokePrivateMethod($object, $method, $args = [], $class = null) { if (is_null($class)) { $class = $object; } $method = new \ReflectionMethod($class, $method); $method->setAccessible(true); return $method->invokeArgs($object, $args); } /** * Returns class name without namespace * * (does not use reflection actually) * * @param $object * @return mixed */ public static function getClassShortName($object) { $path = explode('\\', get_class($object)); return array_pop($path); } } <?php namespace Codeception\Util\Shared; trait Asserts { protected function assert($arguments, $not = false) { $not = $not ? 'Not' : ''; $method = ucfirst(array_shift($arguments)); if (($method === 'True') && $not) { $method = 'False'; $not = ''; } if (($method === 'False') && $not) { $method = 'True'; $not = ''; } call_user_func_array(['\PHPUnit\Framework\Assert', 'assert' . $not . $method], $arguments); } protected function assertNot($arguments) { $this->assert($arguments, true); } /** * Checks that two variables are equal. * * @param $expected * @param $actual * @param string $message * @param float $delta */ protected function assertEquals($expected, $actual, $message = '', $delta = 0.0) { \PHPUnit\Framework\Assert::assertEquals($expected, $actual, $message, $delta); } /** * Checks that two variables are not equal * * @param $expected * @param $actual * @param string $message * @param float $delta */ protected function assertNotEquals($expected, $actual, $message = '', $delta = 0.0) { \PHPUnit\Framework\Assert::assertNotEquals($expected, $actual, $message, $delta); } /** * Checks that two variables are same * * @param $expected * @param $actual * @param string $message */ protected function assertSame($expected, $actual, $message = '') { \PHPUnit\Framework\Assert::assertSame($expected, $actual, $message); } /** * Checks that two variables are not same * * @param $expected * @param $actual * @param string $message */ protected function assertNotSame($expected, $actual, $message = '') { \PHPUnit\Framework\Assert::assertNotSame($expected, $actual, $message); } /** * Checks that actual is greater than expected * * @param $expected * @param $actual * @param string $message */ protected function assertGreaterThan($expected, $actual, $message = '') { \PHPUnit\Framework\Assert::assertGreaterThan($expected, $actual, $message); } /** * Checks that actual is greater or equal than expected * * @param $expected * @param $actual * @param string $message */ protected function assertGreaterThanOrEqual($expected, $actual, $message = '') { \PHPUnit\Framework\Assert::assertGreaterThanOrEqual($expected, $actual, $message); } /** * Checks that actual is less than expected * * @param $expected * @param $actual * @param string $message */ protected function assertLessThan($expected, $actual, $message = '') { \PHPUnit\Framework\Assert::assertLessThan($expected, $actual, $message); } /** * Checks that actual is less or equal than expected * * @param $expected * @param $actual * @param string $message */ protected function assertLessThanOrEqual($expected, $actual, $message = '') { \PHPUnit\Framework\Assert::assertLessThanOrEqual($expected, $actual, $message); } /** * Checks that haystack contains needle * * @param $needle * @param $haystack * @param string $message */ protected function assertContains($needle, $haystack, $message = '') { \PHPUnit\Framework\Assert::assertContains($needle, $haystack, $message); } /** * Checks that haystack doesn't contain needle. * * @param $needle * @param $haystack * @param string $message */ protected function assertNotContains($needle, $haystack, $message = '') { \PHPUnit\Framework\Assert::assertNotContains($needle, $haystack, $message); } /** * Checks that string match with pattern * * @param string $pattern * @param string $string * @param string $message */ protected function assertRegExp($pattern, $string, $message = '') { \PHPUnit\Framework\Assert::assertRegExp($pattern, $string, $message); } /** * Checks that string not match with pattern * * @param string $pattern * @param string $string * @param string $message */ protected function assertNotRegExp($pattern, $string, $message = '') { \PHPUnit\Framework\Assert::assertNotRegExp($pattern, $string, $message); } /** * Checks that a string starts with the given prefix. * * @param string $prefix * @param string $string * @param string $message */ protected function assertStringStartsWith($prefix, $string, $message = '') { \PHPUnit\Framework\Assert::assertStringStartsWith($prefix, $string, $message); } /** * Checks that a string doesn't start with the given prefix. * * @param string $prefix * @param string $string * @param string $message */ protected function assertStringStartsNotWith($prefix, $string, $message = '') { \PHPUnit\Framework\Assert::assertStringStartsNotWith($prefix, $string, $message); } /** * Checks that variable is empty. * * @param $actual * @param string $message */ protected function assertEmpty($actual, $message = '') { \PHPUnit\Framework\Assert::assertEmpty($actual, $message); } /** * Checks that variable is not empty. * * @param $actual * @param string $message */ protected function assertNotEmpty($actual, $message = '') { \PHPUnit\Framework\Assert::assertNotEmpty($actual, $message); } /** * Checks that variable is NULL * * @param $actual * @param string $message */ protected function assertNull($actual, $message = '') { \PHPUnit\Framework\Assert::assertNull($actual, $message); } /** * Checks that variable is not NULL * * @param $actual * @param string $message */ protected function assertNotNull($actual, $message = '') { \PHPUnit\Framework\Assert::assertNotNull($actual, $message); } /** * Checks that condition is positive. * * @param $condition * @param string $message */ protected function assertTrue($condition, $message = '') { \PHPUnit\Framework\Assert::assertTrue($condition, $message); } /** * Checks that the condition is NOT true (everything but true) * * @param $condition * @param string $message */ protected function assertNotTrue($condition, $message = '') { \PHPUnit\Framework\Assert::assertNotTrue($condition, $message); } /** * Checks that condition is negative. * * @param $condition * @param string $message */ protected function assertFalse($condition, $message = '') { \PHPUnit\Framework\Assert::assertFalse($condition, $message); } /** * Checks that the condition is NOT false (everything but false) * * @param $condition * @param string $message */ protected function assertNotFalse($condition, $message = '') { \PHPUnit\Framework\Assert::assertNotFalse($condition, $message); } /** * * @param $haystack * @param $constraint * @param string $message */ protected function assertThat($haystack, $constraint, $message = '') { \PHPUnit\Framework\Assert::assertThat($haystack, $constraint, $message); } /** * Checks that haystack doesn't attend * * @param $haystack * @param $constraint * @param string $message */ protected function assertThatItsNot($haystack, $constraint, $message = '') { $constraint = new \PHPUnit\Framework\Constraint\LogicalNot($constraint); \PHPUnit\Framework\Assert::assertThat($haystack, $constraint, $message); } /** * Checks if file exists * * @param string $filename * @param string $message */ protected function assertFileExists($filename, $message = '') { \PHPUnit\Framework\Assert::assertFileExists($filename, $message); } /** * Checks if file doesn't exist * * @param string $filename * @param string $message */ protected function assertFileNotExists($filename, $message = '') { \PHPUnit\Framework\Assert::assertFileNotExists($filename, $message); } /** * @param $expected * @param $actual * @param $description */ protected function assertGreaterOrEquals($expected, $actual, $description = '') { \PHPUnit\Framework\Assert::assertGreaterThanOrEqual($expected, $actual, $description); } /** * @param $expected * @param $actual * @param $description */ protected function assertLessOrEquals($expected, $actual, $description = '') { \PHPUnit\Framework\Assert::assertLessThanOrEqual($expected, $actual, $description); } /** * @param $actual * @param $description */ protected function assertIsEmpty($actual, $description = '') { \PHPUnit\Framework\Assert::assertEmpty($actual, $description); } /** * @param $key * @param $actual * @param $description */ protected function assertArrayHasKey($key, $actual, $description = '') { \PHPUnit\Framework\Assert::assertArrayHasKey($key, $actual, $description); } /** * @param $key * @param $actual * @param $description */ protected function assertArrayNotHasKey($key, $actual, $description = '') { \PHPUnit\Framework\Assert::assertArrayNotHasKey($key, $actual, $description); } /** * Checks that array contains subset. * * @param array $subset * @param array $array * @param bool $strict * @param string $message */ protected function assertArraySubset($subset, $array, $strict = false, $message = '') { \PHPUnit\Framework\Assert::assertArraySubset($subset, $array, $strict, $message); } /** * @param $expectedCount * @param $actual * @param $description */ protected function assertCount($expectedCount, $actual, $description = '') { \PHPUnit\Framework\Assert::assertCount($expectedCount, $actual, $description); } /** * @param $class * @param $actual * @param $description */ protected function assertInstanceOf($class, $actual, $description = '') { \PHPUnit\Framework\Assert::assertInstanceOf($class, $actual, $description); } /** * @param $class * @param $actual * @param $description */ protected function assertNotInstanceOf($class, $actual, $description = '') { \PHPUnit\Framework\Assert::assertNotInstanceOf($class, $actual, $description); } /** * @param $type * @param $actual * @param $description */ protected function assertInternalType($type, $actual, $description = '') { \PHPUnit\Framework\Assert::assertInternalType($type, $actual, $description); } /** * Fails the test with message. * * @param $message */ protected function fail($message) { \PHPUnit\Framework\Assert::fail($message); } protected function assertStringContainsString($needle, $haystack, $message = '') { \Codeception\PHPUnit\TestCase::assertStringContainsString($needle, $haystack, $message); } protected function assertStringNotContainsString($needle, $haystack, $message = '') { \Codeception\PHPUnit\TestCase::assertStringNotContainsString($needle, $haystack, $message); } protected function assertStringContainsStringIgnoringCase($needle, $haystack, $message = '') { \Codeception\PHPUnit\TestCase::assertStringContainsStringIgnoringCase($needle, $haystack, $message); } protected function assertStringNotContainsStringIgnoringCase($needle, $haystack, $message = '') { \Codeception\PHPUnit\TestCase::assertStringNotContainsStringIgnoringCase($needle, $haystack, $message); } protected function assertIsArray($actual, $message = '') { \Codeception\PHPUnit\TestCase::assertIsArray($actual, $message); } protected function assertIsBool($actual, $message = '') { \Codeception\PHPUnit\TestCase::assertIsBool($actual, $message); } protected function assertIsFloat($actual, $message = '') { \Codeception\PHPUnit\TestCase::assertIsFloat($actual, $message); } protected function assertIsInt($actual, $message = '') { \Codeception\PHPUnit\TestCase::assertIsInt($actual, $message); } protected function assertIsNumeric($actual, $message = '') { \Codeception\PHPUnit\TestCase::assertIsNumeric($actual, $message); } protected function assertIsObject($actual, $message = '') { \Codeception\PHPUnit\TestCase::assertIsObject($actual, $message); } protected function assertIsResource($actual, $message = '') { \Codeception\PHPUnit\TestCase::assertIsResource($actual, $message); } protected function assertIsString($actual, $message = '') { \Codeception\PHPUnit\TestCase::assertIsString($actual, $message); } protected function assertIsScalar($actual, $message = '') { \Codeception\PHPUnit\TestCase::assertIsScalar($actual, $message); } protected function assertIsCallable($actual, $message = '') { \Codeception\PHPUnit\TestCase::assertIsCallable($actual, $message); } protected function assertIsNotArray($actual, $message = '') { \Codeception\PHPUnit\TestCase::assertIsNotArray($actual, $message); } protected function assertIsNotBool($actual, $message = '') { \Codeception\PHPUnit\TestCase::assertIsNotBool($actual, $message); } protected function assertIsNotFloat($actual, $message = '') { \Codeception\PHPUnit\TestCase::assertIsNotFloat($actual, $message); } protected function assertIsNotInt($actual, $message = '') { \Codeception\PHPUnit\TestCase::assertIsNotInt($actual, $message); } protected function assertIsNotNumeric($actual, $message = '') { \Codeception\PHPUnit\TestCase::assertIsNotNumeric($actual, $message); } protected function assertIsNotObject($actual, $message = '') { \Codeception\PHPUnit\TestCase::assertIsNotObject($actual, $message); } protected function assertIsNotResource($actual, $message = '') { \Codeception\PHPUnit\TestCase::assertIsNotResource($actual, $message); } protected function assertIsNotString($actual, $message = '') { \Codeception\PHPUnit\TestCase::assertIsNotString($actual, $message); } protected function assertIsNotScalar($actual, $message = '') { \Codeception\PHPUnit\TestCase::assertIsNotScalar($actual, $message); } protected function assertIsNotCallable($actual, $message = '') { \Codeception\PHPUnit\TestCase::assertIsNotCallable($actual, $message); } } <?php namespace Codeception\Util\Shared; trait Namespaces { protected function breakParts($class) { $class = str_replace('/', '\\', $class); $namespaces = explode('\\', $class); if (count($namespaces)) { $namespaces[0] = ltrim($namespaces[0], '\\'); } if (!$namespaces[0]) { array_shift($namespaces); } // remove empty namespace caused of \\ return $namespaces; } protected function getShortClassName($class) { $namespaces = $this->breakParts($class); return array_pop($namespaces); } protected function getNamespaceString($class) { $namespaces = $this->getNamespaces($class); return implode('\\', $namespaces); } protected function getNamespaceHeader($class) { $str = $this->getNamespaceString($class); if (!$str) { return ""; } return "namespace $str;\n"; } protected function getNamespaces($class) { $namespaces = $this->breakParts($class); array_pop($namespaces); $namespaces = array_filter($namespaces, 'strlen'); return $namespaces; } } <?php namespace Codeception\Util; /** * This class is left for BC compatibility. * Most of its contents moved to parent * * Class Soap * @package Codeception\Util */ class Soap extends Xml { public static function request() { return new XmlBuilder(); } public static function response() { return new XmlBuilder(); } } <?php use Codeception\Module\Sequence; if (!function_exists('sq')) { function sq($id = null) { if ($id and isset(Sequence::$hash[$id])) { return Sequence::$hash[$id]; } $prefix = str_replace('{id}', $id, Sequence::$prefix); $sequence = $prefix . uniqid($id); if ($id) { Sequence::$hash[$id] = $sequence; } return $sequence; } } if (!function_exists('sqs')) { function sqs($id = null) { if ($id and isset(Sequence::$suiteHash[$id])) { return Sequence::$suiteHash[$id]; } $prefix = str_replace('{id}', $id, Sequence::$prefix); $sequence = $prefix . uniqid($id); if ($id) { Sequence::$suiteHash[$id] = $sequence; } return $sequence; } } <?php namespace Codeception\Util; use Codeception\Lib\Notification; use Codeception\Stub\Expected; class Stub extends \Codeception\Stub { public static function never($params = null) { Notification::deprecate("Stub::never is deprecated in favor of \Codeception\Stub\Expected::never"); return Expected::never($params); } public static function once($params = null) { Notification::deprecate("Stub::once is deprecated in favor of \Codeception\Stub\Expected::once"); return Expected::once($params); } public static function atLeastOnce($params = null) { Notification::deprecate("Stub::atLeastOnce is deprecated in favor of \Codeception\Stub\Expected::atLeastOnce"); return Expected::atLeastOnce($params); } public static function exactly($count, $params = null) { Notification::deprecate("Stub::exactly is deprecated in favor of \Codeception\Stub\Expected::exactly"); return Expected::exactly($count, $params); } } <?php namespace Codeception\Util; /** * Basic template engine used for generating initial Cept/Cest/Test files. */ class Template { protected $template; protected $vars = []; protected $placeholderStart; protected $placeholderEnd; /** * Takes a template string * * @param $template */ public function __construct($template, $placeholderStart = '{{', $placeholderEnd = '}}') { $this->template = $template; $this->placeholderStart = $placeholderStart; $this->placeholderEnd = $placeholderEnd; } /** * Replaces {{var}} string with provided value * * @param $var * @param $val * @return $this */ public function place($var, $val) { $this->vars[$var] = $val; return $this; } /** * Sets all template vars * * @param array $vars */ public function setVars(array $vars) { $this->vars = $vars; } public function getVar($name) { if (isset($this->vars[$name])) { return $this->vars[$name]; } } /** * Fills up template string with placed variables. * * @return mixed */ public function produce() { $result = $this->template; $regex = sprintf('~%s([\w\.]+)%s~m', $this->placeholderStart, $this->placeholderEnd); $matched = preg_match_all($regex, $result, $matches, PREG_SET_ORDER); if (!$matched) { return $result; } foreach ($matches as $match) { // fill in placeholders $placeholder = $match[1]; $value = $this->vars; foreach (explode('.', $placeholder) as $segment) { if (is_array($value) && array_key_exists($segment, $value)) { $value = $value[$segment]; } else { continue 2; } } $result = str_replace($this->placeholderStart . $placeholder . $this->placeholderEnd, $value, $result); } return $result; } } <?php namespace Codeception\Util; use GuzzleHttp\Psr7\Uri as Psr7Uri; class Uri { /** * Merges the passed $add argument onto $base. * * If a relative URL is passed as the 'path' part of the $add url * array, the relative URL is mapped using the base 'path' part as * its base. * * @param string $baseUri the base URL * @param string $uri the URL to merge * @return array the merged array */ public static function mergeUrls($baseUri, $uri) { $base = new Psr7Uri($baseUri); $parts = parse_url($uri); //If the relative URL does not parse, attempt to parse the entire URL. //PHP Known bug ( https://bugs.php.net/bug.php?id=70942 ) if ($parts === false) { $parts = parse_url($base.$uri); } if ($parts === false) { throw new \InvalidArgumentException("Invalid URI $uri"); } if (isset($parts['host']) and isset($parts['scheme'])) { // if this is an absolute url, replace with it return $uri; } if (isset($parts['host'])) { $base = $base->withHost($parts['host']); $base = $base->withPath(''); $base = $base->withQuery(''); $base = $base->withFragment(''); } if (isset($parts['path'])) { $path = $parts['path']; $basePath = $base->getPath(); if ((strpos($path, '/') !== 0) && !empty($path)) { if ($basePath) { // if it ends with a slash, relative paths are below it if (preg_match('~/$~', $basePath)) { $path = $basePath . $path; } else { // remove double slashes $dir = rtrim(dirname($basePath), '\\/'); $path = $dir . '/' . $path; } } else { $path = '/' . ltrim($path, '/'); } } $base = $base->withPath($path); $base = $base->withQuery(''); $base = $base->withFragment(''); } if (isset($parts['query'])) { $base = $base->withQuery($parts['query']); $base = $base->withFragment(''); } if (isset($parts['fragment'])) { $base = $base->withFragment($parts['fragment']); } return (string) $base; } /** * Retrieve /path?query#fragment part of URL * @param $url * @return string */ public static function retrieveUri($url) { $uri = new Psr7Uri($url); return (string)(new Psr7Uri()) ->withPath($uri->getPath()) ->withQuery($uri->getQuery()) ->withFragment($uri->getFragment()); } public static function retrieveHost($url) { $urlParts = parse_url($url); if (!isset($urlParts['host']) or !isset($urlParts['scheme'])) { throw new \InvalidArgumentException("Wrong URL passes, host and scheme not set"); } $host = $urlParts['scheme'] . '://' . $urlParts['host']; if (isset($urlParts['port'])) { $host .= ':' . $urlParts['port']; } return $host; } public static function appendPath($url, $path) { $uri = new Psr7Uri($url); $cutUrl = (string)$uri->withQuery('')->withFragment(''); if ($path === '' || $path[0] === '#') { return $cutUrl . $path; } return rtrim($cutUrl, '/') . '/' . ltrim($path, '/'); } } <?php namespace Codeception\Util; class Xml { /** * @static * * @param \DOMDocument $xml * @param \DOMNode $node * @param array $array * * @return \DOMDocument */ public static function arrayToXml(\DOMDocument $xml, \DOMNode $node, $array = []) { foreach ($array as $el => $val) { if (is_array($val)) { self::arrayToXml($xml, $node->$el, $val); } else { $node->appendChild($xml->createElement($el, $val)); } } return $xml; } /** * @static * * @param $xml * * @return \DOMDocument|\DOMNode */ public static function toXml($xml) { if ($xml instanceof XmlBuilder) { return $xml->getDom(); } if ($xml instanceof \DOMDocument) { return $xml; } $dom = new \DOMDocument(); $dom->preserveWhiteSpace = false; if ($xml instanceof \DOMNode) { $xml = $dom->importNode($xml, true); $dom->appendChild($xml); return $dom; } if (is_array($xml)) { return self::arrayToXml($dom, $dom, $xml); } if (!empty($xml)) { $dom->loadXML($xml); } return $dom; } public static function build() { return new XmlBuilder(); } } <?php namespace Codeception\Util; /** * That's a pretty simple yet powerful class to build XML structures in jQuery-like style. * With no XML line actually written! * Uses DOM extension to manipulate XML data. * * * ```php * <?php * $xml = new \Codeception\Util\XmlBuilder(); * $xml->users * ->user * ->val(1) * ->email * ->val('davert@mail.ua') * ->attr('valid','true') * ->parent() * ->cart * ->attr('empty','false') * ->items * ->item * ->val('useful item'); * ->parents('user') * ->active * ->val(1); * echo $xml; * ``` * * This will produce this XML * * ```xml * <?xml version="1.0"?> * <users> * <user> * 1 * <email valid="true">davert@mail.ua</email> * <cart empty="false"> * <items> * <item>useful item</item> * </items> * </cart> * <active>1</active> * </user> * </users> * ``` * * ### Usage * * Builder uses chained calls. So each call to builder returns a builder object. * Except for `getDom` and `__toString` methods. * * * `$xml->node` - create new xml node and go inside of it. * * `$xml->node->val('value')` - sets the inner value of node * * `$xml->attr('name','value')` - set the attribute of node * * `$xml->parent()` - go back to parent node. * * `$xml->parents('user')` - go back through all parents to `user` node. * * Export: * * * `$xml->getDom` - get a DOMDocument object * * `$xml->__toString` - get a string representation of XML. * * [Source code](https://github.com/Codeception/Codeception/blob/master/src/Codeception/Util/XmlBuilder.php) */ class XmlBuilder { /** * @var \DOMDocument */ protected $__dom__; /** * @var \DOMElement */ protected $__currentNode__; public function __construct() { $this->__dom__ = new \DOMDocument(); $this->__currentNode__ = $this->__dom__; } /** * Appends child node * * @param $tag * * @return XmlBuilder */ public function __get($tag) { $node = $this->__dom__->createElement($tag); $this->__currentNode__->appendChild($node); $this->__currentNode__ = $node; return $this; } /** * @param $val * * @return XmlBuilder */ public function val($val) { $this->__currentNode__->nodeValue = $val; return $this; } /** * Sets attribute for current node * * @param $attr * @param $val * * @return XmlBuilder */ public function attr($attr, $val) { $this->__currentNode__->setAttribute($attr, $val); return $this; } /** * Traverses to parent * * @return XmlBuilder */ public function parent() { $this->__currentNode__ = $this->__currentNode__->parentNode; return $this; } /** * Traverses to parent with $name * * @param $tag * * @return XmlBuilder * @throws \Exception */ public function parents($tag) { $traverseNode = $this->__currentNode__; $elFound = false; while ($traverseNode->parentNode) { $traverseNode = $traverseNode->parentNode; if ($traverseNode->tagName == $tag) { $this->__currentNode__ = $traverseNode; $elFound = true; break; } } if (!$elFound) { throw new \Exception("Parent $tag not found in XML"); } return $this; } public function __toString() { return $this->__dom__->saveXML(); } /** * @return \DOMDocument */ public function getDom() { return $this->__dom__; } } <?php namespace Codeception\Util; use Codeception\Exception\ElementNotFound; use Codeception\Exception\MalformedLocatorException; use Symfony\Component\CssSelector\CssSelectorConverter; use Symfony\Component\CssSelector\Exception\ParseException; use Codeception\Util\Soap as XmlUtils; class XmlStructure { /** * @var \DOMDocument|\DOMNode */ protected $xml; public function __construct($xml) { $this->xml = XmlUtils::toXml($xml); } public function matchesXpath($xpath) { $path = new \DOMXPath($this->xml); $res = $path->query($xpath); if ($res === false) { throw new MalformedLocatorException($xpath); } return $res->length > 0; } /** * @param $cssOrXPath * @return \DOMElement */ public function matchElement($cssOrXPath) { $xpath = new \DOMXpath($this->xml); try { $selector = (new CssSelectorConverter())->toXPath($cssOrXPath); $els = $xpath->query($selector); if ($els) { return $els->item(0); } } catch (ParseException $e) { } $els = $xpath->query($cssOrXPath); if ($els->length) { return $els->item(0); } throw new ElementNotFound($cssOrXPath); } /** * @param $xml * @return bool */ public function matchXmlStructure($xml) { $xml = XmlUtils::toXml($xml); $root = $xml->firstChild; $els = $this->xml->getElementsByTagName($root->nodeName); if (empty($els)) { throw new ElementNotFound($root->nodeName, 'Element'); } $matches = false; foreach ($els as $node) { $matches |= $this->matchForNode($root, $node); } return $matches; } protected function matchForNode($schema, $xml) { foreach ($schema->childNodes as $node1) { $matched = false; foreach ($xml->childNodes as $node2) { if ($node1->nodeName == $node2->nodeName) { $matched = $this->matchForNode($node1, $node2); if ($matched) { break; } } } if (!$matched) { return false; } } return true; } } <?php namespace Codeception\Extension; use Codeception\Event\FailEvent; use Codeception\Events; use Codeception\Extension; use Codeception\Subscriber\Console; /** * DotReporter provides less verbose output for test execution. * Like PHPUnit printer it prints dots "." for successful testes and "F" for failures. * * ![](https://cloud.githubusercontent.com/assets/220264/26132800/4d23f336-3aab-11e7-81ba-2896a4c623d2.png) * * ```bash * .......... * .......... * .......... * .......... * .......... * .......... * .......... * .......... * * Time: 2.07 seconds, Memory: 20.00MB * * OK (80 tests, 124 assertions) * ``` * * * Enable this reporter with `--ext option` * * ``` * codecept run --ext DotReporter * ``` * * Failures and Errors are printed by a standard Codeception reporter. * Use this extension as an example for building custom reporters. */ class DotReporter extends Extension { /** * @var Console */ protected $standardReporter; protected $errors = []; protected $failures = []; protected $width = 10; protected $currentPos = 0; public function _initialize() { $this->options['silent'] = false; // turn on printing for this extension $this->_reconfigure(['settings' => ['silent' => true]]); // turn off printing for everything else $this->standardReporter = new Console($this->options); $this->width = $this->standardReporter->detectWidth(); } // we are listening for events public static $events = [ Events::SUITE_BEFORE => 'beforeSuite', Events::TEST_SUCCESS => 'success', Events::TEST_FAIL => 'fail', Events::TEST_ERROR => 'error', Events::TEST_SKIPPED => 'skipped', Events::TEST_FAIL_PRINT => 'printFailed' ]; public function beforeSuite() { $this->writeln(""); } public function success() { $this->printChar('.'); } public function fail(FailEvent $e) { $this->printChar("<error>F</error>"); } public function error(FailEvent $e) { $this->printChar('<error>E</error>'); } public function skipped() { $this->printChar('S'); } protected function printChar($char) { if ($this->currentPos >= $this->width) { $this->writeln(''); $this->currentPos = 0; } $this->write($char); $this->currentPos++; } public function printFailed(FailEvent $event) { $this->standardReporter->printFail($event); } } <?php namespace Codeception\Extension; use Codeception\Event\FailEvent; use Codeception\Event\StepEvent; use Codeception\Event\SuiteEvent; use Codeception\Event\TestEvent; use Codeception\Events; use Codeception\Exception\ConfigurationException; use Codeception\Exception\ExtensionException; use Codeception\Extension; use Codeception\Test\Descriptor; use Monolog\Formatter\LineFormatter; use Monolog\Handler\RotatingFileHandler; /** * Log suites/tests/steps using Monolog library. * Monolog should be installed additionally by Composer. * * ``` * composer require monolog/monolog * ``` * * Steps are logged into `tests/_output/codeception.log` * * To enable this module add to your `codeception.yml`: * * ``` yaml * extensions: * enabled: [Codeception\Extension\Logger] * ``` * * #### Config * * * `max_files` (default: 3) - how many log files to keep * */ class Logger extends Extension { public static $events = [ Events::SUITE_BEFORE => 'beforeSuite', Events::TEST_BEFORE => 'beforeTest', Events::TEST_AFTER => 'afterTest', Events::TEST_END => 'endTest', Events::STEP_BEFORE => 'beforeStep', Events::TEST_FAIL => 'testFail', Events::TEST_ERROR => 'testError', Events::TEST_INCOMPLETE => 'testIncomplete', Events::TEST_SKIPPED => 'testSkipped', ]; protected $logHandler; /** * @var \Monolog\Logger */ protected static $logger; protected $path; protected $config = ['max_files' => 3]; public function _initialize() { if (!class_exists('\Monolog\Logger')) { throw new ConfigurationException("Logger extension requires Monolog library to be installed"); } $this->path = $this->getLogDir(); // internal log $logHandler = new RotatingFileHandler($this->path . 'codeception.log', $this->config['max_files']); $formatter = $logHandler->getFormatter(); if ($formatter instanceof LineFormatter) { $formatter->ignoreEmptyContextAndExtra(true); } self::$logger = new \Monolog\Logger('Codeception'); self::$logger->pushHandler($logHandler); } public static function getLogger() { return self::$logger; } public function beforeSuite(SuiteEvent $e) { $suite = str_replace('\\', '_', $e->getSuite()->getName()); $this->logHandler = new RotatingFileHandler($this->path . $suite, $this->config['max_files']); } public function beforeTest(TestEvent $e) { self::$logger = new \Monolog\Logger(Descriptor::getTestFileName($e->getTest())); self::$logger->pushHandler($this->logHandler); self::$logger->info('------------------------------------'); self::$logger->info("STARTED: " . ucfirst(Descriptor::getTestAsString($e->getTest()))); } public function afterTest(TestEvent $e) { } public function endTest(TestEvent $e) { self::$logger->info("PASSED"); } public function testFail(FailEvent $e) { self::$logger->alert($e->getFail()->getMessage()); self::$logger->info("# FAILED #"); } public function testError(FailEvent $e) { self::$logger->alert($e->getFail()->getMessage()); self::$logger->info("# ERROR #"); } public function testSkipped(FailEvent $e) { self::$logger->info("# Skipped #"); } public function testIncomplete(FailEvent $e) { self::$logger->info("# Incomplete #"); } public function beforeStep(StepEvent $e) { self::$logger->info((string) $e->getStep()); } } if (!function_exists('codecept_log')) { function codecept_log() { return Logger::getLogger(); } } else { throw new ExtensionException('Codeception\Extension\Logger', "function 'codecept_log' already defined"); } <?php namespace Codeception\Extension; use Codeception\Event\StepEvent; use Codeception\Event\TestEvent; use Codeception\Events; use Codeception\Exception\ExtensionException; use Codeception\Lib\Interfaces\ScreenshotSaver; use Codeception\Module\WebDriver; use Codeception\Step; use Codeception\Step\Comment as CommentStep; use Codeception\Test\Descriptor; use Codeception\Util\FileSystem; use Codeception\Util\Template; /** * Saves a screenshot of each step in acceptance tests and shows them as a slideshow on one HTML page (here's an [example](http://codeception.com/images/recorder.gif)) * Activated only for suites with WebDriver module enabled. * * The screenshots are saved to `tests/_output/record_*` directories, open `index.html` to see them as a slideshow. * * #### Installation * * Add this to the list of enabled extensions in `codeception.yml` or `acceptance.suite.yml`: * * ``` yaml * extensions: * enabled: * - Codeception\Extension\Recorder * ``` * * #### Configuration * * * `delete_successful` (default: true) - delete screenshots for successfully passed tests (i.e. log only failed and errored tests). * * `module` (default: WebDriver) - which module for screenshots to use. Set `AngularJS` if you want to use it with AngularJS module. Generally, the module should implement `Codeception\Lib\Interfaces\ScreenshotSaver` interface. * * `ignore_steps` (default: []) - array of step names that should not be recorded (given the step passed), * wildcards supported. Meta steps can also be ignored. * * `success_color` (default: success) - bootstrap values to be used for color representation for passed tests * * `failure_color` (default: danger) - bootstrap values to be used for color representation for failed tests * * `error_color` (default: dark) - bootstrap values to be used for color representation for scenarios where there's an issue occurred while generating a recording * * `delete_orphaned` (default: false) - delete recording folders created via previous runs * * `include_microseconds` (default: false) - enable microsecond precision for recorded step time details * * #### Examples: * * ``` yaml * extensions: * enabled: * - Codeception\Extension\Recorder: * module: AngularJS # enable for Angular * delete_successful: false # keep screenshots of successful tests * ignore_steps: [have, grab*] * ``` * #### Skipping recording of steps with annotations * * It is also possible to skip recording of steps for specified tests by using the @skipRecording annotation. * * ```php * /** * * @skipRecording login * * @skipRecording amOnUrl * *\/ * public function testLogin(AcceptanceTester $I) * { * $I->login(); * $I->amOnUrl('http://codeception.com'); * } * ``` * */ class Recorder extends \Codeception\Extension { /** @var array */ protected $config = [ 'delete_successful' => true, 'module' => 'WebDriver', 'template' => null, 'animate_slides' => true, 'ignore_steps' => [], 'success_color' => 'success', 'failure_color' => 'danger', 'error_color' => 'dark', 'delete_orphaned' => false, 'include_microseconds' => false, ]; /** @var string */ protected $template = <<<EOF <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Recorder Result EOF; /** @var string */ protected $indicatorTemplate = << EOF; /** @var string */ protected $indexTemplate = << Recorder Results Index

Record #{{seed}}

    {{records}}
EOF; /** @var string */ protected $slidesTemplate = <<
EOF; /** @var array */ public static $events = [ Events::SUITE_BEFORE => 'beforeSuite', Events::SUITE_AFTER => 'afterSuite', Events::TEST_BEFORE => 'before', Events::TEST_ERROR => 'persist', Events::TEST_FAIL => 'persist', Events::TEST_SUCCESS => 'cleanup', Events::STEP_AFTER => 'afterStep', ]; /** @var WebDriver */ protected $webDriverModule; /** @var string */ protected $dir; /** @var array */ protected $slides = []; /** @var int */ protected $stepNum = 0; /** @var string */ protected $seed; /** @var array */ protected $seeds; /** @var array */ protected $recordedTests = []; /** @var array */ protected $skipRecording = []; /** @var array */ protected $errorMessages = []; /** @var bool */ protected $colors; /** @var bool */ protected $ansi; /** @var array */ protected $timeStamps = []; /** @var string */ private $dateFormat; public function beforeSuite() { $this->webDriverModule = null; if (!$this->hasModule($this->config['module'])) { $this->writeln('Recorder is disabled, no available modules'); return; } $this->seed = uniqid(); $this->seeds[] = $this->seed; $this->webDriverModule = $this->getModule($this->config['module']); $this->skipRecording = []; $this->errorMessages = []; $this->dateFormat = $this->config['include_microseconds'] ? 'Y-m-d\TH:i:s.uP' : DATE_ATOM; $this->ansi = !isset($this->options['no-ansi']); $this->colors = !isset($this->options['no-colors']); if (!$this->webDriverModule instanceof ScreenshotSaver) { throw new ExtensionException( $this, 'You should pass module which implements ' . ScreenshotSaver::class . ' interface' ); } $this->writeln( sprintf( '⏺ Recording ⏺ step-by-step screenshots will be saved to %s', codecept_output_dir() ) ); $this->writeln("Directory Format: record_{$this->seed}_{filename}_{testname} ----"); } public function afterSuite() { if (!$this->webDriverModule) { return; } $links = ''; if (count($this->slides)) { foreach ($this->recordedTests as $suiteName => $suite) { $links .= "
  • {$suiteName}
    • "; foreach ($suite as $fileName => $tests) { $links .= "
    • {$fileName}
      • "; foreach ($tests as $test) { $links .= in_array($test['path'], $this->skipRecording, true) ? "
      • config['error_color']}\">{$test['name']}
      • \n" : '
      • {$test['name']}
      • \n"; } $links .= '
      '; } $links .= '
'; } $indexHTML = (new Template($this->indexTemplate)) ->place('seed', $this->seed) ->place('records', $links) ->produce(); try { file_put_contents(codecept_output_dir() . 'records.html', $indexHTML); } catch (\Exception $exception) { $this->writeln( "⏺ An exception occurred while saving records.html: {$exception->getMessage()}" ); } $this->writeln('⏺ Records saved into: file://' . codecept_output_dir() . 'records.html'); } foreach ($this->errorMessages as $message) { $this->writeln($message); } } /** * @param TestEvent $e */ public function before(TestEvent $e) { if (!$this->webDriverModule) { return; } $this->dir = null; $this->stepNum = 0; $this->slides = []; $this->timeStamps = []; $this->dir = codecept_output_dir() . "record_{$this->seed}_{$this->getTestName($e)}"; $testPath = codecept_relative_path(Descriptor::getTestFullName($e->getTest())); try { !is_dir($this->dir) && !mkdir($this->dir) && !is_dir($this->dir); } catch (\Exception $exception) { $this->skipRecording[] = $testPath; $this->appendErrorMessage( $testPath, "⏺ An exception occurred while creating directory: {$this->dir}" ); } } /** * @param TestEvent $e */ public function cleanup(TestEvent $e) { if ($this->config['delete_orphaned']) { $recordingDirectories = []; $directories = new \DirectoryIterator(codecept_output_dir()); // getting a list of currently present recording directories foreach ($directories as $directory) { preg_match('/^record_(.*?)_[^\n]+.php_[^\n]+$/', $directory->getFilename(), $match); if (isset($match[1])) { $recordingDirectories[$match[1]][] = codecept_output_dir() . $directory->getFilename(); } } // removing orphaned recording directories foreach (array_diff(array_keys($recordingDirectories), $this->seeds) as $orphanedSeed) { foreach ($recordingDirectories[$orphanedSeed] as $orphanedDirectory) { FileSystem::deleteDir($orphanedDirectory); } } } if (!$this->webDriverModule || !$this->dir) { return; } if (!$this->config['delete_successful']) { $this->persist($e); return; } // deleting successfully executed tests FileSystem::deleteDir($this->dir); } /** * @param TestEvent $e */ public function persist(TestEvent $e) { if (!$this->webDriverModule) { return; } $indicatorHtml = ''; $slideHtml = ''; $testName = $this->getTestName($e); $testPath = codecept_relative_path(Descriptor::getTestFullName($e->getTest())); $dir = codecept_output_dir() . "record_{$this->seed}_$testName"; $status = 'success'; if (strcasecmp($this->dir, $dir) !== 0) { $filename = str_pad(0, 3, '0', STR_PAD_LEFT) . '.png'; try { !is_dir($dir) && !mkdir($dir) && !is_dir($dir); $this->dir = $dir; } catch (\Exception $exception) { $this->skipRecording[] = $testPath; $this->appendErrorMessage( $testPath, "⏺ An exception occurred while creating directory: {$dir}" ); } $this->slides = []; $this->timeStamps = []; $this->slides[$filename] = new Step\Action('encountered an unexpected error prior to the test execution'); $this->timeStamps[$filename] = (new \DateTime())->format($this->dateFormat); $status = 'error'; try { if ($this->webDriverModule->webDriver === null) { throw new ExtensionException($this, 'Failed to save screenshot as webDriver is not set'); } $this->webDriverModule->webDriver->takeScreenshot($this->dir . DIRECTORY_SEPARATOR . $filename); } catch (\Exception $exception) { $this->appendErrorMessage( $testPath, "⏺ Unable to capture a screenshot for {$testPath}/before" ); } } if (!in_array($testPath, $this->skipRecording, true)) { foreach ($this->slides as $i => $step) { /** @var Step $step */ if ($step->hasFailed()) { $status = 'failure'; } $indicatorHtml .= (new Template($this->indicatorTemplate)) ->place('step', (int)$i) ->place('isActive', (int)$i ? '' : 'active') ->produce(); $slideHtml .= (new Template($this->slidesTemplate)) ->place('image', $i) ->place('caption', $step->getHtml('#3498db')) ->place('isActive', (int)$i ? '' : 'active') ->place('isError', $status === 'success' ? '' : 'error') ->place('timeStamp', $this->timeStamps[$i]) ->produce(); } $html = (new Template($this->template)) ->place('indicators', $indicatorHtml) ->place('slides', $slideHtml) ->place('feature', ucfirst($e->getTest()->getFeature())) ->place('test', Descriptor::getTestSignature($e->getTest())) ->place('carousel_class', $this->config['animate_slides'] ? ' slide' : '') ->produce(); $indexFile = $this->dir . DIRECTORY_SEPARATOR . 'index.html'; $environment = $e->getTest()->getMetadata()->getCurrent('env') ?: ''; $suite = ucfirst(basename(\dirname($e->getTest()->getMetadata()->getFilename()))); $testName = basename($e->getTest()->getMetadata()->getFilename()); try { file_put_contents($indexFile, $html); } catch (\Exception $exception) { $this->skipRecording[] = $testPath; $this->appendErrorMessage( $testPath, "⏺ An exception occurred while saving index.html for {$testPath}: " . "{$exception->getMessage()}" ); } $this->recordedTests["{$suite} ({$environment})"][$testName][] = [ 'name' => $e->getTest()->getMetadata()->getName(), 'path' => $testPath, 'status' => $status, 'index' => substr($indexFile, strlen(codecept_output_dir())), ]; } } /** * @param StepEvent $e */ public function afterStep(StepEvent $e) { if ($this->webDriverModule === null || $this->dir === null) { return; } if ($e->getStep() instanceof CommentStep) { return; } // only taking the ignore step into consideration if that step has passed if ($this->isStepIgnored($e) && !$e->getStep()->hasFailed()) { return; } $filename = str_pad($this->stepNum, 3, '0', STR_PAD_LEFT) . '.png'; try { if ($this->webDriverModule->webDriver === null) { throw new ExtensionException($this, 'Failed to save screenshot as webDriver is not set'); } $this->webDriverModule->webDriver->takeScreenshot($this->dir . DIRECTORY_SEPARATOR . $filename); } catch (\Exception $exception) { $testPath = codecept_relative_path(Descriptor::getTestFullName($e->getTest())); $this->appendErrorMessage( $testPath, "⏺ Unable to capture a screenshot for {$testPath}/{$e->getStep()->getAction()}" ); } $this->stepNum++; $this->slides[$filename] = $e->getStep(); $this->timeStamps[$filename] = (new \DateTime())->format($this->dateFormat); } /** * @param StepEvent $e * * @return bool */ protected function isStepIgnored(StepEvent $e) { $configIgnoredSteps = $this->config['ignore_steps']; $annotationIgnoredSteps = $e->getTest()->getMetadata()->getParam('skipRecording'); $ignoredSteps = array_unique( array_merge( $configIgnoredSteps, is_array($annotationIgnoredSteps) ? $annotationIgnoredSteps : [] ) ); foreach ($ignoredSteps as $stepPattern) { $stepRegexp = '/^' . str_replace('*', '.*?', $stepPattern) . '$/i'; if (preg_match($stepRegexp, $e->getStep()->getAction())) { return true; } if ($e->getStep()->getMetaStep() !== null && preg_match($stepRegexp, $e->getStep()->getMetaStep()->getAction()) ) { return true; } } return false; } /** * @param StepEvent|TestEvent $e * * @return string */ private function getTestName($e) { return basename($e->getTest()->getMetadata()->getFilename()) . '_' . $e->getTest()->getMetadata()->getName(); } /** * @param string $message */ protected function writeln($message) { parent::writeln( $this->ansi ? $message : trim(preg_replace('/[ ]{2,}/', ' ', str_replace('⏺', '', $message))) ); } /** * @param string $testPath * @param string $message */ private function appendErrorMessage($testPath, $message) { $this->errorMessages[$testPath] = array_merge( array_key_exists($testPath, $this->errorMessages) ? $this->errorMessages[$testPath]: [], [$message] ); } } 'runBefore' ]; /** @var array[] */ private $processes = []; public function _initialize() { if (!class_exists('Symfony\Component\Process\Process')) { throw new ExtensionException($this, 'symfony/process package is required'); } } public function runBefore() { $this->runProcesses(); $this->processMonitoring(); } private function runProcesses() { foreach ($this->config as $item) { if (is_array($item)) { $currentCommand = array_shift($item); $followingCommands = $item; } else { $currentCommand = $item; $followingCommands = []; } $process = $this->runProcess($currentCommand); $this->addProcessToMonitoring($process, $followingCommands); } } /** * @param string $command * @return Process */ private function runProcess($command) { $this->output->debug('[RunBefore] Starting ' . $command); $process = new Process($command, $this->getRootDir()); $process->start(); return $process; } /** * @param string[] $followingCommands */ private function addProcessToMonitoring(Process $process, array $followingCommands) { $this->processes[] = [ 'instance' => $process, 'following' => $followingCommands ]; } /** * @param int $index */ private function removeProcessFromMonitoring($index) { unset($this->processes[$index]); } private function processMonitoring() { while (count($this->processes) !== 0) { $this->checkProcesses(); sleep(1); } } private function checkProcesses() { foreach ($this->processes as $index => $process) { if (!$this->isRunning($process['instance'])) { $this->output->debug('[RunBefore] Completing ' . $process['instance']->getCommandLine()); $this->runFollowingCommand($process['following']); $this->removeProcessFromMonitoring($index); } } } /** * @param string[] $followingCommands */ private function runFollowingCommand(array $followingCommands) { if (count($followingCommands) > 0) { $process = $this->runProcess(array_shift($followingCommands)); $this->addProcessToMonitoring($process, $followingCommands); } } private function isRunning(Process $process) { if ($process->isRunning()) { return true; } return false; } } 'saveFailed' ]; /** @var string filename/groupname for failed tests */ protected $group = 'failed'; public function _initialize() { if (array_key_exists('fail-group', $this->config) && $this->config['fail-group']) { $this->group = $this->config['fail-group']; } $logPath = str_replace($this->getRootDir(), '', $this->getLogDir()); // get local path to logs $this->_reconfigure(['groups' => [$this->group => $logPath . $this->group]]); } public function saveFailed(PrintResultEvent $e) { $file = $this->getLogDir() . $this->group; $result = $e->getResult(); if ($result->wasSuccessful()) { if (is_file($file)) { unlink($file); } return; } $output = []; foreach ($result->failures() as $fail) { $output[] = $this->localizePath(Descriptor::getTestFullName($fail->failedTest())); } foreach ($result->errors() as $fail) { $output[] = $this->localizePath(Descriptor::getTestFullName($fail->failedTest())); } file_put_contents($file, implode("\n", $output)); } protected function localizePath($path) { $root = realpath($this->getRootDir()) . DIRECTORY_SEPARATOR; if (substr($path, 0, strlen($root)) == $root) { return substr($path, strlen($root)); } return $path; } } 0]; static $events = [ Events::SUITE_BEFORE => 'runProcess', Events::SUITE_AFTER => 'stopProcess' ]; protected $processes = []; public function _initialize() { if (!class_exists('Symfony\Component\Process\Process')) { throw new ExtensionException($this, 'symfony/process package is required'); } } public function runProcess() { $this->processes = []; foreach ($this->config as $key => $command) { if (!$command) { continue; } if (!is_int($key)) { continue; // configuration options } $process = new Process($command, $this->getRootDir(), null, null, null); $process->start(); $this->processes[] = $process; $this->output->debug('[RunProcess] Starting '.$command); } sleep($this->config['sleep']); } public function __destruct() { $this->stopProcess(); } public function stopProcess() { foreach (array_reverse($this->processes) as $process) { /** @var $process Process **/ if (!$process->isRunning()) { continue; } $this->output->debug('[RunProcess] Stopping ' . $process->getCommandLine()); $process->stop(); } $this->processes = []; } } options['silent'] = false; // turn on printing for this extension $this->_reconfigure(['settings' => ['silent' => true]]); // turn off printing for everything else } // we are listening for events public static $events = [ Events::SUITE_BEFORE => 'beforeSuite', Events::TEST_END => 'after', Events::TEST_SUCCESS => 'success', Events::TEST_FAIL => 'fail', Events::TEST_ERROR => 'error', ]; public function beforeSuite() { $this->writeln(""); } public function success() { $this->write('[+] '); } public function fail() { $this->write('[-] '); } public function error() { $this->write('[E] '); } // we are printing test status and time taken public function after(TestEvent $e) { $seconds_input = $e->getTime(); // stack overflow: http://stackoverflow.com/questions/16825240/how-to-convert-microtime-to-hhmmssuu $seconds = (int)($milliseconds = (int)($seconds_input * 1000)) / 1000; $time = ($seconds % 60) . (($milliseconds === 0) ? '' : '.' . $milliseconds); $this->write(Descriptor::getTestSignature($e->getTest())); $this->writeln(' (' . $time . 's)'); } } array ( 'and' => 'And|*', 'background' => 'Background', 'but' => 'But|*', 'examples' => 'Scenarios|Examples', 'feature' => 'Business Need|Feature|Ability', 'given' => 'Given|*', 'name' => 'English', 'native' => 'English', 'rule' => 'Rule', 'scenario' => 'Scenario|Example', 'scenario_outline' => 'Scenario Template|Scenario Outline', 'then' => 'Then|*', 'when' => 'When|*', ), 'af' => array ( 'and' => 'En|*', 'background' => 'Agtergrond', 'but' => 'Maar|*', 'examples' => 'Voorbeelde', 'feature' => 'Besigheid Behoefte|Funksie|Vermoë', 'given' => 'Gegewe|*', 'name' => 'Afrikaans', 'native' => 'Afrikaans', 'rule' => 'Rule', 'scenario' => 'Voorbeeld|Situasie', 'scenario_outline' => 'Situasie Uiteensetting', 'then' => 'Dan|*', 'when' => 'Wanneer|*', ), 'am' => array ( 'and' => 'Եվ|*', 'background' => 'Կոնտեքստ', 'but' => 'Բայց|*', 'examples' => 'Օրինակներ', 'feature' => 'Ֆունկցիոնալություն|Հատկություն', 'given' => 'Դիցուք|*', 'name' => 'Armenian', 'native' => 'հայերեն', 'rule' => 'Rule', 'scenario' => 'Օրինակ|Սցենար', 'scenario_outline' => 'Սցենարի կառուցվացքը', 'then' => 'Ապա|*', 'when' => 'Երբ|Եթե|*', ), 'an' => array ( 'and' => '*|Y|E', 'background' => 'Antecedents', 'but' => 'Pero|*', 'examples' => 'Eixemplos', 'feature' => 'Caracteristica', 'given' => 'Dadas|Dada|Daus|Dau|*', 'name' => 'Aragonese', 'native' => 'Aragonés', 'rule' => 'Rule', 'scenario' => 'Eixemplo|Caso', 'scenario_outline' => 'Esquema del caso', 'then' => 'Antonces|Allora|Alavez|*', 'when' => 'Cuan|*', ), 'ar' => array ( 'and' => '*|و', 'background' => 'الخلفية', 'but' => 'لكن|*', 'examples' => 'امثلة', 'feature' => 'خاصية', 'given' => 'بفرض|*', 'name' => 'Arabic', 'native' => 'العربية', 'rule' => 'Rule', 'scenario' => 'سيناريو|مثال', 'scenario_outline' => 'سيناريو مخطط', 'then' => 'اذاً|ثم|*', 'when' => 'عندما|متى|*', ), 'ast' => array ( 'and' => 'Ya|*|Y', 'background' => 'Antecedentes', 'but' => 'Peru|*', 'examples' => 'Exemplos', 'feature' => 'Carauterística', 'given' => 'Dada|Daos|Daes|Dáu|*', 'name' => 'Asturian', 'native' => 'asturianu', 'rule' => 'Rule', 'scenario' => 'Exemplo|Casu', 'scenario_outline' => 'Esbozu del casu', 'then' => 'Entós|*', 'when' => 'Cuando|*', ), 'az' => array ( 'and' => 'Həm|Və|*', 'background' => 'Kontekst|Keçmiş', 'but' => 'Ancaq|Amma|*', 'examples' => 'Nümunələr', 'feature' => 'Özəllik', 'given' => 'Tutaq ki|Verilir|*', 'name' => 'Azerbaijani', 'native' => 'Azərbaycanca', 'rule' => 'Rule', 'scenario' => 'Nümunələr|Ssenari', 'scenario_outline' => 'Ssenarinin strukturu', 'then' => 'O halda|*', 'when' => 'Nə vaxt ki|Əgər|*', ), 'bg' => array ( 'and' => '*|И', 'background' => 'Предистория', 'but' => 'Но|*', 'examples' => 'Примери', 'feature' => 'Функционалност', 'given' => 'Дадено|*', 'name' => 'Bulgarian', 'native' => 'български', 'rule' => 'Rule', 'scenario' => 'Сценарий|Пример', 'scenario_outline' => 'Рамка на сценарий', 'then' => 'То|*', 'when' => 'Когато|*', ), 'bm' => array ( 'and' => 'Dan|*', 'background' => 'Latar Belakang', 'but' => 'Tetapi|Tapi|*', 'examples' => 'Contoh', 'feature' => 'Fungsi', 'given' => 'Diberi|Bagi|*', 'name' => 'Malay', 'native' => 'Bahasa Melayu', 'rule' => 'Rule', 'scenario' => 'Senario|Situasi|Keadaan', 'scenario_outline' => 'Garis Panduan Senario|Kerangka Senario|Kerangka Situasi|Kerangka Keadaan', 'then' => 'Kemudian|Maka|*', 'when' => 'Apabila|*', ), 'bs' => array ( 'and' => '*|I|A', 'background' => 'Pozadina', 'but' => 'Ali|*', 'examples' => 'Primjeri', 'feature' => 'Karakteristika', 'given' => 'Dato|*', 'name' => 'Bosnian', 'native' => 'Bosanski', 'rule' => 'Rule', 'scenario' => 'Scenariju|Scenario|Primjer', 'scenario_outline' => 'Scenario-outline|Scenariju-obris', 'then' => 'Zatim|*', 'when' => 'Kada|*', ), 'ca' => array ( 'and' => '*|I', 'background' => 'Antecedents|Rerefons', 'but' => 'Però|*', 'examples' => 'Exemples', 'feature' => 'Característica|Funcionalitat', 'given' => 'Donada|Donat|Atesa|Atès|*', 'name' => 'Catalan', 'native' => 'català', 'rule' => 'Rule', 'scenario' => 'Escenari|Exemple', 'scenario_outline' => 'Esquema de l\'escenari', 'then' => 'Aleshores|Cal|*', 'when' => 'Quan|*', ), 'cs' => array ( 'and' => 'A také|*|A', 'background' => 'Kontext|Pozadí', 'but' => 'Ale|*', 'examples' => 'Příklady', 'feature' => 'Požadavek', 'given' => 'Za předpokladu|Pokud|*', 'name' => 'Czech', 'native' => 'Česky', 'rule' => 'Rule', 'scenario' => 'Příklad|Scénář', 'scenario_outline' => 'Osnova scénáře|Náčrt Scénáře', 'then' => 'Pak|*', 'when' => 'Když|*', ), 'cy-GB' => array ( 'and' => '*|A', 'background' => 'Cefndir', 'but' => 'Ond|*', 'examples' => 'Enghreifftiau', 'feature' => 'Arwedd', 'given' => 'Anrhegedig a|*', 'name' => 'Welsh', 'native' => 'Cymraeg', 'rule' => 'Rule', 'scenario' => 'Enghraifft|Scenario', 'scenario_outline' => 'Scenario Amlinellol', 'then' => 'Yna|*', 'when' => 'Pryd|*', ), 'da' => array ( 'and' => 'Og|*', 'background' => 'Baggrund', 'but' => 'Men|*', 'examples' => 'Eksempler', 'feature' => 'Egenskab', 'given' => 'Givet|*', 'name' => 'Danish', 'native' => 'dansk', 'rule' => 'Rule', 'scenario' => 'Eksempel|Scenarie', 'scenario_outline' => 'Abstrakt Scenario', 'then' => 'Så|*', 'when' => 'Når|*', ), 'de' => array ( 'and' => 'Und|*', 'background' => 'Grundlage', 'but' => 'Aber|*', 'examples' => 'Beispiele', 'feature' => 'Funktionalität', 'given' => 'Gegeben seien|Gegeben sei|Angenommen|*', 'name' => 'German', 'native' => 'Deutsch', 'rule' => 'Rule', 'scenario' => 'Beispiel|Szenario', 'scenario_outline' => 'Szenariogrundriss', 'then' => 'Dann|*', 'when' => 'Wenn|*', ), 'el' => array ( 'and' => 'Και|*', 'background' => 'Υπόβαθρο', 'but' => 'Αλλά|*', 'examples' => 'Παραδείγματα|Σενάρια', 'feature' => 'Δυνατότητα|Λειτουργία', 'given' => 'Δεδομένου|*', 'name' => 'Greek', 'native' => 'Ελληνικά', 'rule' => 'Rule', 'scenario' => 'Παράδειγμα|Σενάριο', 'scenario_outline' => 'Περίγραμμα Σεναρίου|Περιγραφή Σεναρίου', 'then' => 'Τότε|*', 'when' => 'Όταν|*', ), 'em' => array ( 'and' => '😂<|*', 'background' => '💤', 'but' => '😔<|*', 'examples' => '📓', 'feature' => '📚', 'given' => '😐<|*', 'name' => 'Emoji', 'native' => '😀', 'rule' => 'Rule', 'scenario' => '🥒|📕', 'scenario_outline' => '📖', 'then' => '🙏<|*', 'when' => '🎬<|*', ), 'en-Scouse' => array ( 'and' => 'An|*', 'background' => 'Dis is what went down', 'but' => 'Buh|*', 'examples' => 'Examples', 'feature' => 'Feature', 'given' => 'Youse know when youse got|Givun|*', 'name' => 'Scouse', 'native' => 'Scouse', 'rule' => 'Rule', 'scenario' => 'The thing of it is', 'scenario_outline' => 'Wharrimean is', 'then' => 'Den youse gotta|Dun|*', 'when' => 'Youse know like when|Wun|*', ), 'en-au' => array ( 'and' => 'Too right|*', 'background' => 'First off', 'but' => 'Yeah nah|*', 'examples' => 'You\'ll wanna', 'feature' => 'Pretty much', 'given' => 'Y\'know|*', 'name' => 'Australian', 'native' => 'Australian', 'rule' => 'Rule', 'scenario' => 'Awww, look mate', 'scenario_outline' => 'Reckon it\'s like', 'then' => 'But at the end of the day I reckon|*', 'when' => 'It\'s just unbelievable|*', ), 'en-lol' => array ( 'and' => 'AN|*', 'background' => 'B4', 'but' => 'BUT|*', 'examples' => 'EXAMPLZ', 'feature' => 'OH HAI', 'given' => 'I CAN HAZ|*', 'name' => 'LOLCAT', 'native' => 'LOLCAT', 'rule' => 'Rule', 'scenario' => 'MISHUN', 'scenario_outline' => 'MISHUN SRSLY', 'then' => 'DEN|*', 'when' => 'WEN|*', ), 'en-old' => array ( 'and' => 'Ond|*|7', 'background' => 'Aer|Ær', 'but' => 'Ac|*', 'examples' => 'Se the|Se þe|Se ðe', 'feature' => 'Hwaet|Hwæt', 'given' => 'Thurh|Þurh|Ðurh|*', 'name' => 'Old English', 'native' => 'Englisc', 'rule' => 'Rule', 'scenario' => 'Swa', 'scenario_outline' => 'Swa hwaer swa|Swa hwær swa', 'then' => 'Tha the|Þa þe|Ða ðe|Tha|Þa|Ða|*', 'when' => 'Tha|Þa|Ða|*', ), 'en-pirate' => array ( 'and' => 'Aye|*', 'background' => 'Yo-ho-ho', 'but' => 'Avast!|*', 'examples' => 'Dead men tell no tales', 'feature' => 'Ahoy matey!', 'given' => 'Gangway!|*', 'name' => 'Pirate', 'native' => 'Pirate', 'rule' => 'Rule', 'scenario' => 'Heave to', 'scenario_outline' => 'Shiver me timbers', 'then' => 'Let go and haul|*', 'when' => 'Blimey!|*', ), 'eo' => array ( 'and' => 'Kaj|*', 'background' => 'Fono', 'but' => 'Sed|*', 'examples' => 'Ekzemploj', 'feature' => 'Trajto', 'given' => 'Donitaĵo|Komence|*', 'name' => 'Esperanto', 'native' => 'Esperanto', 'rule' => 'Rule', 'scenario' => 'Ekzemplo|Scenaro|Kazo', 'scenario_outline' => 'Konturo de la scenaro|Kazo-skizo|Skizo', 'then' => 'Do|*', 'when' => 'Se|*', ), 'es' => array ( 'and' => '*|Y|E', 'background' => 'Antecedentes', 'but' => 'Pero|*', 'examples' => 'Ejemplos', 'feature' => 'Característica', 'given' => 'Dados|Dadas|Dada|Dado|*', 'name' => 'Spanish', 'native' => 'español', 'rule' => 'Rule', 'scenario' => 'Escenario|Ejemplo', 'scenario_outline' => 'Esquema del escenario', 'then' => 'Entonces|*', 'when' => 'Cuando|*', ), 'et' => array ( 'and' => 'Ja|*', 'background' => 'Taust', 'but' => 'Kuid|*', 'examples' => 'Juhtumid', 'feature' => 'Omadus', 'given' => 'Eeldades|*', 'name' => 'Estonian', 'native' => 'eesti keel', 'rule' => 'Rule', 'scenario' => 'Stsenaarium|Juhtum', 'scenario_outline' => 'Raamstsenaarium|Raamstjuhtum', 'then' => 'Siis|*', 'when' => 'Kui|*', ), 'fa' => array ( 'and' => '*|و', 'background' => 'زمینه', 'but' => 'اما|*', 'examples' => 'نمونه ها', 'feature' => 'وِیژگی', 'given' => 'با فرض|*', 'name' => 'Persian', 'native' => 'فارسی', 'rule' => 'Rule', 'scenario' => 'سناریو|مثال', 'scenario_outline' => 'الگوی سناریو', 'then' => 'آنگاه|*', 'when' => 'هنگامی|*', ), 'fi' => array ( 'and' => 'Ja|*', 'background' => 'Tausta', 'but' => 'Mutta|*', 'examples' => 'Tapaukset', 'feature' => 'Ominaisuus', 'given' => 'Oletetaan|*', 'name' => 'Finnish', 'native' => 'suomi', 'rule' => 'Rule', 'scenario' => 'Tapaus', 'scenario_outline' => 'Tapausaihio', 'then' => 'Niin|*', 'when' => 'Kun|*', ), 'fr' => array ( 'and' => 'Et qu\'<|Et que|Et|*', 'background' => 'Contexte', 'but' => 'Mais qu\'<|Mais que|Mais|*', 'examples' => 'Exemples', 'feature' => 'Fonctionnalité', 'given' => 'Etant donné qu\'<|Étant donné qu\'<|Etant donné que|Étant donné que|Etant données|Étant données|Etant donnée|Etant donnés|Étant donnée|Étant donnés|Etant donné|Étant donné|Soit|*', 'name' => 'French', 'native' => 'français', 'rule' => 'Règle', 'scenario' => 'Scénario|Exemple', 'scenario_outline' => 'Plan du scénario|Plan du Scénario', 'then' => 'Alors|*', 'when' => 'Lorsqu\'<|Lorsque|Quand|*', ), 'ga' => array ( 'and' => 'Agus<|*', 'background' => 'Cúlra', 'but' => 'Ach<|*', 'examples' => 'Samplaí', 'feature' => 'Gné', 'given' => 'Cuir i gcás nach<|Cuir i gcás gur<|Cuir i gcás nár<|Cuir i gcás go<|*', 'name' => 'Irish', 'native' => 'Gaeilge', 'rule' => 'Rule', 'scenario' => 'Sampla|Cás', 'scenario_outline' => 'Cás Achomair', 'then' => 'Ansin<|*', 'when' => 'Nuair nach<|Nuair nár<|Nuair ba<|Nuair a<|*', ), 'gj' => array ( 'and' => 'અને|*', 'background' => 'બેકગ્રાઉન્ડ', 'but' => 'પણ|*', 'examples' => 'ઉદાહરણો', 'feature' => 'વ્યાપાર જરૂર|ક્ષમતા|લક્ષણ', 'given' => 'આપેલ છે|*', 'name' => 'Gujarati', 'native' => 'ગુજરાતી', 'rule' => 'Rule', 'scenario' => 'ઉદાહરણ|સ્થિતિ', 'scenario_outline' => 'પરિદ્દશ્ય રૂપરેખા|પરિદ્દશ્ય ઢાંચો', 'then' => 'પછી|*', 'when' => 'ક્યારે|*', ), 'gl' => array ( 'and' => '*|E', 'background' => 'Contexto', 'but' => 'Pero|Mais|*', 'examples' => 'Exemplos', 'feature' => 'Característica', 'given' => 'Dados|Dadas|Dada|Dado|*', 'name' => 'Galician', 'native' => 'galego', 'rule' => 'Rule', 'scenario' => 'Escenario|Exemplo', 'scenario_outline' => 'Esbozo do escenario', 'then' => 'Entón|Logo|*', 'when' => 'Cando|*', ), 'he' => array ( 'and' => 'וגם|*', 'background' => 'רקע', 'but' => 'אבל|*', 'examples' => 'דוגמאות', 'feature' => 'תכונה', 'given' => 'בהינתן|*', 'name' => 'Hebrew', 'native' => 'עברית', 'rule' => 'Rule', 'scenario' => 'דוגמא|תרחיש', 'scenario_outline' => 'תבנית תרחיש', 'then' => 'אזי|אז|*', 'when' => 'כאשר|*', ), 'hi' => array ( 'and' => 'तथा|और|*', 'background' => 'पृष्ठभूमि', 'but' => 'परन्तु|किन्तु|पर|*', 'examples' => 'उदाहरण', 'feature' => 'रूप लेख', 'given' => 'चूंकि|यदि|अगर|*', 'name' => 'Hindi', 'native' => 'हिंदी', 'rule' => 'Rule', 'scenario' => 'परिदृश्य', 'scenario_outline' => 'परिदृश्य रूपरेखा', 'then' => 'तदा|तब|*', 'when' => 'कदा|जब|*', ), 'hr' => array ( 'and' => '*|I', 'background' => 'Pozadina', 'but' => 'Ali|*', 'examples' => 'Scenariji|Primjeri', 'feature' => 'Mogucnost|Mogućnost|Osobina', 'given' => 'Ukoliko|Zadani|Zadano|Zadan|*', 'name' => 'Croatian', 'native' => 'hrvatski', 'rule' => 'Rule', 'scenario' => 'Scenarij|Primjer', 'scenario_outline' => 'Koncept|Skica', 'then' => 'Onda|*', 'when' => 'Kada|Kad|*', ), 'ht' => array ( 'and' => 'Epi|Ak|*|E', 'background' => 'Kontèks|Istorik', 'but' => 'Men|*', 'examples' => 'Egzanp', 'feature' => 'Karakteristik|Fonksyonalite|Mak', 'given' => 'Sipoze ke|Sipoze Ke|Sipoze|*', 'name' => 'Creole', 'native' => 'kreyòl', 'rule' => 'Rule', 'scenario' => 'Senaryo', 'scenario_outline' => 'Senaryo deskripsyon|Senaryo Deskripsyon|Dyagram senaryo|Dyagram Senaryo|Plan senaryo|Plan Senaryo', 'then' => 'Le sa a|Lè sa a|*', 'when' => 'Le|Lè|*', ), 'hu' => array ( 'and' => 'És|*', 'background' => 'Háttér', 'but' => 'De|*', 'examples' => 'Példák', 'feature' => 'Jellemző', 'given' => 'Amennyiben|Adott|*', 'name' => 'Hungarian', 'native' => 'magyar', 'rule' => 'Rule', 'scenario' => 'Forgatókönyv|Példa', 'scenario_outline' => 'Forgatókönyv vázlat', 'then' => 'Akkor|*', 'when' => 'Amikor|Majd|Ha|*', ), 'id' => array ( 'and' => 'Dan|*', 'background' => 'Dasar', 'but' => 'Tapi|*', 'examples' => 'Contoh', 'feature' => 'Fitur', 'given' => 'Dengan|*', 'name' => 'Indonesian', 'native' => 'Bahasa Indonesia', 'rule' => 'Rule', 'scenario' => 'Skenario', 'scenario_outline' => 'Skenario konsep', 'then' => 'Maka|*', 'when' => 'Ketika|*', ), 'is' => array ( 'and' => 'Og|*', 'background' => 'Bakgrunnur', 'but' => 'En|*', 'examples' => 'Atburðarásir|Dæmi', 'feature' => 'Eiginleiki', 'given' => 'Ef|*', 'name' => 'Icelandic', 'native' => 'Íslenska', 'rule' => 'Rule', 'scenario' => 'Atburðarás', 'scenario_outline' => 'Lýsing Atburðarásar|Lýsing Dæma', 'then' => 'Þá|*', 'when' => 'Þegar|*', ), 'it' => array ( 'and' => '*|E', 'background' => 'Contesto', 'but' => 'Ma|*', 'examples' => 'Esempi', 'feature' => 'Funzionalità', 'given' => 'Data|Dato|Dati|Date|*', 'name' => 'Italian', 'native' => 'italiano', 'rule' => 'Rule', 'scenario' => 'Scenario|Esempio', 'scenario_outline' => 'Schema dello scenario', 'then' => 'Allora|*', 'when' => 'Quando|*', ), 'ja' => array ( 'and' => 'かつ<|*', 'background' => '背景', 'but' => 'しかし<|ただし<|但し<|*', 'examples' => 'サンプル|例', 'feature' => 'フィーチャ|機能', 'given' => '前提<|*', 'name' => 'Japanese', 'native' => '日本語', 'rule' => 'Rule', 'scenario' => 'シナリオ', 'scenario_outline' => 'シナリオアウトライン|シナリオテンプレート|シナリオテンプレ|テンプレ', 'then' => 'ならば<|*', 'when' => 'もし<|*', ), 'jv' => array ( 'and' => 'Lan|*', 'background' => 'Dasar', 'but' => 'Ananging|Nanging|Tapi|*', 'examples' => 'Contone|Conto', 'feature' => 'Fitur', 'given' => 'Nalikaning|Nalika|*', 'name' => 'Javanese', 'native' => 'Basa Jawa', 'rule' => 'Rule', 'scenario' => 'Skenario', 'scenario_outline' => 'Konsep skenario', 'then' => 'Banjur|Njuk|*', 'when' => 'Menawa|Manawa|*', ), 'ka' => array ( 'and' => 'და<|*', 'background' => 'კონტექსტი', 'but' => 'მაგ­რამ<|*', 'examples' => 'მაგალითები', 'feature' => 'თვისება', 'given' => 'მოცემული<|*', 'name' => 'Georgian', 'native' => 'ქართველი', 'rule' => 'Rule', 'scenario' => 'მაგალითად|სცენარის', 'scenario_outline' => 'სცენარის ნიმუში', 'then' => 'მაშინ<|*', 'when' => 'როდესაც<|*', ), 'kn' => array ( 'and' => 'ಮತ್ತು|*', 'background' => 'ಹಿನ್ನೆಲೆ', 'but' => 'ಆದರೆ|*', 'examples' => 'ಉದಾಹರಣೆಗಳು', 'feature' => 'ಹೆಚ್ಚಳ', 'given' => 'ನೀಡಿದ|*', 'name' => 'Kannada', 'native' => 'ಕನ್ನಡ', 'rule' => 'Rule', 'scenario' => 'ಕಥಾಸಾರಾಂಶ|ಉದಾಹರಣೆ', 'scenario_outline' => 'ವಿವರಣೆ', 'then' => 'ನಂತರ|*', 'when' => 'ಸ್ಥಿತಿಯನ್ನು|*', ), 'ko' => array ( 'and' => '그리고<|*', 'background' => '배경', 'but' => '하지만<|단<|*', 'examples' => '예', 'feature' => '기능', 'given' => '먼저<|조건<|*', 'name' => 'Korean', 'native' => '한국어', 'rule' => 'Rule', 'scenario' => '시나리오', 'scenario_outline' => '시나리오 개요', 'then' => '그러면<|*', 'when' => '만약<|만일<|*', ), 'lt' => array ( 'and' => 'Ir|*', 'background' => 'Kontekstas', 'but' => 'Bet|*', 'examples' => 'Pavyzdžiai|Scenarijai|Variantai', 'feature' => 'Savybė', 'given' => 'Duota|*', 'name' => 'Lithuanian', 'native' => 'lietuvių kalba', 'rule' => 'Rule', 'scenario' => 'Scenarijus|Pavyzdys', 'scenario_outline' => 'Scenarijaus šablonas', 'then' => 'Tada|*', 'when' => 'Kai|*', ), 'lu' => array ( 'and' => 'an|*|a', 'background' => 'Hannergrond', 'but' => 'awer|mä|*', 'examples' => 'Beispiller', 'feature' => 'Funktionalitéit', 'given' => 'ugeholl|*', 'name' => 'Luxemburgish', 'native' => 'Lëtzebuergesch', 'rule' => 'Rule', 'scenario' => 'Beispill|Szenario', 'scenario_outline' => 'Plang vum Szenario', 'then' => 'dann|*', 'when' => 'wann|*', ), 'lv' => array ( 'and' => 'Un|*', 'background' => 'Konteksts|Situācija', 'but' => 'Bet|*', 'examples' => 'Piemēri|Paraugs', 'feature' => 'Funkcionalitāte|Fīča', 'given' => 'Kad|*', 'name' => 'Latvian', 'native' => 'latviešu', 'rule' => 'Rule', 'scenario' => 'Scenārijs|Piemērs', 'scenario_outline' => 'Scenārijs pēc parauga', 'then' => 'Tad|*', 'when' => 'Ja|*', ), 'mk-Cyrl' => array ( 'and' => '*|И', 'background' => 'Контекст|Содржина', 'but' => 'Но|*', 'examples' => 'Сценарија|Примери', 'feature' => 'Функционалност|Бизнис потреба|Можност', 'given' => 'Дадена|Дадено|*', 'name' => 'Macedonian', 'native' => 'Македонски', 'rule' => 'Rule', 'scenario' => 'На пример|Сценарио|Пример', 'scenario_outline' => 'Преглед на сценарија|Концепт|Скица', 'then' => 'Тогаш|*', 'when' => 'Кога|*', ), 'mk-Latn' => array ( 'and' => '*|I', 'background' => 'Sodrzhina|Kontekst', 'but' => 'No|*', 'examples' => 'Scenaria|Primeri', 'feature' => 'Funkcionalnost|Biznis potreba|Mozhnost', 'given' => 'Dadena|Dadeno|*', 'name' => 'Macedonian (Latin)', 'native' => 'Makedonski (Latinica)', 'rule' => 'Rule', 'scenario' => 'Na primer|Scenario', 'scenario_outline' => 'Pregled na scenarija|Koncept|Skica', 'then' => 'Togash|*', 'when' => 'Koga|*', ), 'mn' => array ( 'and' => 'Тэгээд|Мөн|*', 'background' => 'Агуулга', 'but' => 'Гэхдээ|Харин|*', 'examples' => 'Тухайлбал', 'feature' => 'Функционал|Функц', 'given' => 'Өгөгдсөн нь|Анх|*', 'name' => 'Mongolian', 'native' => 'монгол', 'rule' => 'Rule', 'scenario' => 'Сценар', 'scenario_outline' => 'Сценарын төлөвлөгөө', 'then' => 'Үүний дараа|Тэгэхэд|*', 'when' => 'Хэрэв|*', ), 'nl' => array ( 'and' => 'En|*', 'background' => 'Achtergrond', 'but' => 'Maar|*', 'examples' => 'Voorbeelden', 'feature' => 'Functionaliteit', 'given' => 'Gegeven|Stel|*', 'name' => 'Dutch', 'native' => 'Nederlands', 'rule' => 'Rule', 'scenario' => 'Voorbeeld|Scenario', 'scenario_outline' => 'Abstract Scenario', 'then' => 'Dan|*', 'when' => 'Wanneer|Als|*', ), 'no' => array ( 'and' => 'Og|*', 'background' => 'Bakgrunn', 'but' => 'Men|*', 'examples' => 'Eksempler', 'feature' => 'Egenskap', 'given' => 'Gitt|*', 'name' => 'Norwegian', 'native' => 'norsk', 'rule' => 'Regel', 'scenario' => 'Eksempel|Scenario', 'scenario_outline' => 'Abstrakt Scenario|Scenariomal', 'then' => 'Så|*', 'when' => 'Når|*', ), 'pa' => array ( 'and' => 'ਅਤੇ|*', 'background' => 'ਪਿਛੋਕੜ', 'but' => 'ਪਰ|*', 'examples' => 'ਉਦਾਹਰਨਾਂ', 'feature' => 'ਨਕਸ਼ ਨੁਹਾਰ|ਮੁਹਾਂਦਰਾ|ਖਾਸੀਅਤ', 'given' => 'ਜਿਵੇਂ ਕਿ|ਜੇਕਰ|*', 'name' => 'Panjabi', 'native' => 'ਪੰਜਾਬੀ', 'rule' => 'Rule', 'scenario' => 'ਉਦਾਹਰਨ|ਪਟਕਥਾ', 'scenario_outline' => 'ਪਟਕਥਾ ਰੂਪ ਰੇਖਾ|ਪਟਕਥਾ ਢਾਂਚਾ', 'then' => 'ਤਦ|*', 'when' => 'ਜਦੋਂ|*', ), 'pl' => array ( 'and' => 'Oraz|*|I', 'background' => 'Założenia', 'but' => 'Ale|*', 'examples' => 'Przykłady', 'feature' => 'Potrzeba biznesowa|Właściwość|Funkcja|Aspekt', 'given' => 'Zakładając, że|Zakładając|Mając|*', 'name' => 'Polish', 'native' => 'polski', 'rule' => 'Rule', 'scenario' => 'Scenariusz|Przykład', 'scenario_outline' => 'Szablon scenariusza', 'then' => 'Wtedy|*', 'when' => 'Jeżeli|Jeśli|Kiedy|Gdy|*', ), 'pt' => array ( 'and' => '*|E', 'background' => 'Cenario de Fundo|Cenário de Fundo|Contexto|Fundo', 'but' => 'Mas|*', 'examples' => 'Exemplos|Cenários|Cenarios', 'feature' => 'Funcionalidade|Característica|Caracteristica', 'given' => 'Dados|Dadas|Dada|Dado|*', 'name' => 'Portuguese', 'native' => 'português', 'rule' => 'Rule', 'scenario' => 'Exemplo|Cenário|Cenario', 'scenario_outline' => 'Delineação do Cenário|Delineacao do Cenario|Esquema do Cenário|Esquema do Cenario', 'then' => 'Entao|Então|*', 'when' => 'Quando|*', ), 'ro' => array ( 'and' => 'Și|Si|Şi|*', 'background' => 'Context', 'but' => 'Dar|*', 'examples' => 'Exemple', 'feature' => 'Functionalitate|Funcționalitate|Funcţionalitate', 'given' => 'Dată fiind<|Date fiind|Dati fiind|Dați fiind|Daţi fiind|Dat fiind|*', 'name' => 'Romanian', 'native' => 'română', 'rule' => 'Rule', 'scenario' => 'Scenariu|Exemplu', 'scenario_outline' => 'Structura scenariu|Structură scenariu', 'then' => 'Atunci|*', 'when' => 'Când|Cand|*', ), 'ru' => array ( 'and' => 'К тому же|Также|*|И', 'background' => 'Предыстория|Контекст', 'but' => 'Иначе|Но|*|А', 'examples' => 'Примеры', 'feature' => 'Функциональность|Функционал|Свойство|Функция', 'given' => 'Допустим|Пусть|Дано|*', 'name' => 'Russian', 'native' => 'русский', 'rule' => 'Rule', 'scenario' => 'Сценарий|Пример', 'scenario_outline' => 'Структура сценария', 'then' => 'Затем|Тогда|То|*', 'when' => 'Когда|Если|*', ), 'sk' => array ( 'and' => 'A taktiež|A zároveň|A tiež|*|A', 'background' => 'Pozadie', 'but' => 'Ale|*', 'examples' => 'Príklady', 'feature' => 'Požiadavka|Vlastnosť|Funkcia', 'given' => 'Za predpokladu|Pokiaľ|*', 'name' => 'Slovak', 'native' => 'Slovensky', 'rule' => 'Rule', 'scenario' => 'Príklad|Scenár', 'scenario_outline' => 'Osnova Scenára|Náčrt Scenáru|Náčrt Scenára', 'then' => 'Potom|Tak|*', 'when' => 'Keď|Ak|*', ), 'sl' => array ( 'and' => 'Ter|In', 'background' => 'Kontekst|Osnova|Ozadje', 'but' => 'Vendar|Ampak|Toda', 'examples' => 'Scenariji|Primeri', 'feature' => 'Funkcionalnost|Značilnost|Funkcija|Možnosti|Moznosti|Lastnost', 'given' => 'Privzeto|Zaradi|Podano|Dano', 'name' => 'Slovenian', 'native' => 'Slovenski', 'rule' => 'Rule', 'scenario' => 'Scenarij|Primer', 'scenario_outline' => 'Struktura scenarija|Oris scenarija|Koncept|Osnutek|Skica', 'then' => 'Takrat|Potem|Nato', 'when' => 'Kadar|Ko|Ce|Če', ), 'sr-Cyrl' => array ( 'and' => '*|И', 'background' => 'Контекст|Позадина|Основа', 'but' => 'Али|*', 'examples' => 'Сценарији|Примери', 'feature' => 'Функционалност|Могућност|Особина', 'given' => 'За дате|За дато|За дати|*', 'name' => 'Serbian', 'native' => 'Српски', 'rule' => 'Rule', 'scenario' => 'Сценарио|Пример|Пример', 'scenario_outline' => 'Структура сценарија|Концепт|Скица', 'then' => 'Онда|*', 'when' => 'Када|Кад|*', ), 'sr-Latn' => array ( 'and' => '*|I', 'background' => 'Kontekst|Pozadina|Osnova', 'but' => 'Ali|*', 'examples' => 'Scenariji|Primeri', 'feature' => 'Funkcionalnost|Mogućnost|Mogucnost|Osobina', 'given' => 'Za date|Za dato|Za dati|*', 'name' => 'Serbian (Latin)', 'native' => 'Srpski (Latinica)', 'rule' => 'Rule', 'scenario' => 'Scenario|Primer', 'scenario_outline' => 'Struktura scenarija|Koncept|Skica', 'then' => 'Onda|*', 'when' => 'Kada|Kad|*', ), 'sv' => array ( 'and' => 'Och|*', 'background' => 'Bakgrund', 'but' => 'Men|*', 'examples' => 'Exempel', 'feature' => 'Egenskap', 'given' => 'Givet|*', 'name' => 'Swedish', 'native' => 'Svenska', 'rule' => 'Rule', 'scenario' => 'Scenario', 'scenario_outline' => 'Abstrakt Scenario|Scenariomall', 'then' => 'Så|*', 'when' => 'När|*', ), 'ta' => array ( 'and' => 'மற்றும்|மேலும்|*', 'background' => 'பின்னணி', 'but' => 'ஆனால்|*', 'examples' => 'எடுத்துக்காட்டுகள்|நிலைமைகளில்|காட்சிகள்', 'feature' => 'வணிக தேவை|அம்சம்|திறன்', 'given' => 'கொடுக்கப்பட்ட|*', 'name' => 'Tamil', 'native' => 'தமிழ்', 'rule' => 'Rule', 'scenario' => 'உதாரணமாக|காட்சி', 'scenario_outline' => 'காட்சி வார்ப்புரு|காட்சி சுருக்கம்', 'then' => 'அப்பொழுது|*', 'when' => 'எப்போது|*', ), 'th' => array ( 'and' => 'และ|*', 'background' => 'แนวคิด', 'but' => 'แต่|*', 'examples' => 'ชุดของเหตุการณ์|ชุดของตัวอย่าง', 'feature' => 'ความต้องการทางธุรกิจ|ความสามารถ|โครงหลัก', 'given' => 'กำหนดให้|*', 'name' => 'Thai', 'native' => 'ไทย', 'rule' => 'Rule', 'scenario' => 'เหตุการณ์', 'scenario_outline' => 'โครงสร้างของเหตุการณ์|สรุปเหตุการณ์', 'then' => 'ดังนั้น|*', 'when' => 'เมื่อ|*', ), 'tl' => array ( 'and' => 'మరియు|*', 'background' => 'నేపథ్యం', 'but' => 'కాని|*', 'examples' => 'ఉదాహరణలు', 'feature' => 'గుణము', 'given' => 'చెప్పబడినది|*', 'name' => 'Telugu', 'native' => 'తెలుగు', 'rule' => 'Rule', 'scenario' => 'సన్నివేశం|ఉదాహరణ', 'scenario_outline' => 'కథనం', 'then' => 'అప్పుడు|*', 'when' => 'ఈ పరిస్థితిలో|*', ), 'tlh' => array ( 'and' => 'latlh|\'ej|*', 'background' => 'mo\'', 'but' => '\'ach|\'a|*', 'examples' => 'ghantoH|lutmey', 'feature' => 'poQbogh malja\'|Qu\'meH \'ut|perbogh|Qap|laH', 'given' => 'DaH ghu\' bejlu\'|ghu\' noblu\'|*', 'name' => 'Klingon', 'native' => 'tlhIngan', 'rule' => 'Rule', 'scenario' => 'lut', 'scenario_outline' => 'lut chovnatlh', 'then' => 'vaj|*', 'when' => 'qaSDI\'|*', ), 'tr' => array ( 'and' => 'Ve|*', 'background' => 'Geçmiş', 'but' => 'Fakat|Ama|*', 'examples' => 'Örnekler', 'feature' => 'Özellik', 'given' => 'Diyelim ki|*', 'name' => 'Turkish', 'native' => 'Türkçe', 'rule' => 'Rule', 'scenario' => 'Senaryo|Örnek', 'scenario_outline' => 'Senaryo taslağı', 'then' => 'O zaman|*', 'when' => 'Eğer ki|*', ), 'tt' => array ( 'and' => 'Һәм|Вә|*', 'background' => 'Кереш', 'but' => 'Ләкин|Әмма|*', 'examples' => 'Үрнәкләр|Мисаллар', 'feature' => 'Үзенчәлеклелек|Мөмкинлек', 'given' => 'Әйтик|*', 'name' => 'Tatar', 'native' => 'Татарча', 'rule' => 'Rule', 'scenario' => 'Сценарий', 'scenario_outline' => 'Сценарийның төзелеше', 'then' => 'Нәтиҗәдә|*', 'when' => 'Әгәр|*', ), 'uk' => array ( 'and' => 'А також|Та|*|І', 'background' => 'Передумова', 'but' => 'Але|*', 'examples' => 'Приклади', 'feature' => 'Функціонал', 'given' => 'Припустимо, що|Припустимо|Нехай|Дано|*', 'name' => 'Ukrainian', 'native' => 'Українська', 'rule' => 'Rule', 'scenario' => 'Сценарій|Приклад', 'scenario_outline' => 'Структура сценарію', 'then' => 'Тоді|То|*', 'when' => 'Коли|Якщо|*', ), 'ur' => array ( 'and' => 'اور|*', 'background' => 'پس منظر', 'but' => 'لیکن|*', 'examples' => 'مثالیں', 'feature' => 'کاروبار کی ضرورت|صلاحیت|خصوصیت', 'given' => 'فرض کیا|بالفرض|اگر|*', 'name' => 'Urdu', 'native' => 'اردو', 'rule' => 'Rule', 'scenario' => 'منظرنامہ', 'scenario_outline' => 'منظر نامے کا خاکہ', 'then' => 'پھر|تب|*', 'when' => 'جب|*', ), 'uz' => array ( 'and' => 'Ва|*', 'background' => 'Тарих', 'but' => 'Бирок|Лекин|Аммо|*', 'examples' => 'Мисоллар', 'feature' => 'Функционал', 'given' => 'Агар|*', 'name' => 'Uzbek', 'native' => 'Узбекча', 'rule' => 'Rule', 'scenario' => 'Сценарий', 'scenario_outline' => 'Сценарий структураси', 'then' => 'Унда|*', 'when' => 'Агар|*', ), 'vi' => array ( 'and' => 'Và|*', 'background' => 'Bối cảnh', 'but' => 'Nhưng|*', 'examples' => 'Dữ liệu', 'feature' => 'Tính năng', 'given' => 'Biết|Cho|*', 'name' => 'Vietnamese', 'native' => 'Tiếng Việt', 'rule' => 'Rule', 'scenario' => 'Tình huống|Kịch bản', 'scenario_outline' => 'Khung tình huống|Khung kịch bản', 'then' => 'Thì|*', 'when' => 'Khi|*', ), 'zh-CN' => array ( 'and' => '并且<|而且<|同时<|*', 'background' => '背景', 'but' => '但是<|*', 'examples' => '例子', 'feature' => '功能', 'given' => '假设<|假如<|假定<|*', 'name' => 'Chinese simplified', 'native' => '简体中文', 'rule' => 'Rule', 'scenario' => '场景|剧本', 'scenario_outline' => '场景大纲|剧本大纲', 'then' => '那么<|*', 'when' => '当<|*', ), 'zh-TW' => array ( 'and' => '並且<|而且<|同時<|*', 'background' => '背景', 'but' => '但是<|*', 'examples' => '例子', 'feature' => '功能', 'given' => '假設<|假如<|假定<|*', 'name' => 'Chinese traditional', 'native' => '繁體中文', 'rule' => 'Rule', 'scenario' => '場景|劇本', 'scenario_outline' => '場景大綱|劇本大綱', 'then' => '那麼<|*', 'when' => '當<|*', ), );path = rtrim($path, DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR.'v'.Gherkin::VERSION; if (!is_dir($this->path)) { @mkdir($this->path, 0777, true); } if (!is_writeable($this->path)) { throw new CacheException(sprintf('Cache path "%s" is not writeable. Check your filesystem permissions or disable Gherkin file cache.', $this->path)); } } public function isFresh($path, $timestamp) { $cachePath = $this->getCachePathFor($path); if (!file_exists($cachePath)) { return false; } return filemtime($cachePath) > $timestamp; } public function read($path) { $cachePath = $this->getCachePathFor($path); $feature = unserialize(file_get_contents($cachePath)); if (!$feature instanceof FeatureNode) { throw new CacheException(sprintf('Can not load cache for a feature "%s" from "%s".', $path, $cachePath )); } return $feature; } public function write($path, FeatureNode $feature) { file_put_contents($this->getCachePathFor($path), serialize($feature)); } protected function getCachePathFor($path) { return $this->path.'/'.md5($path).'.feature.cache'; } } features[$path])) { return false; } return $this->timestamps[$path] > $timestamp; } public function read($path) { return $this->features[$path]; } public function write($path, FeatureNode $feature) { $this->features[$path] = $feature; $this->timestamps[$path] = time(); } } getScenarios() as $scenario) { if (!$this->isScenarioMatch($feature, $scenario)) { continue; } $scenarios[] = $scenario; } return new FeatureNode( $feature->getTitle(), $feature->getDescription(), $feature->getTags(), $feature->getBackground(), $scenarios, $feature->getKeyword(), $feature->getLanguage(), $feature->getFile(), $feature->getLine() ); } } filterLine = intval($filterLine); } public function isFeatureMatch(FeatureNode $feature) { return $this->filterLine === $feature->getLine(); } public function isScenarioMatch(ScenarioInterface $scenario) { if ($this->filterLine === $scenario->getLine()) { return true; } if ($scenario instanceof OutlineNode && $scenario->hasExamples()) { return $this->filterLine === $scenario->getLine() || in_array($this->filterLine, $scenario->getExampleTable()->getLines()); } return false; } public function filterFeature(FeatureNode $feature) { $scenarios = array(); foreach ($feature->getScenarios() as $scenario) { if (!$this->isScenarioMatch($scenario)) { continue; } if ($scenario instanceof OutlineNode && $scenario->hasExamples()) { $table = $scenario->getExampleTable()->getTable(); $lines = array_keys($table); if (in_array($this->filterLine, $lines)) { $filteredTable = array($lines[0] => $table[$lines[0]]); if ($lines[0] !== $this->filterLine) { $filteredTable[$this->filterLine] = $table[$this->filterLine]; } $scenario = new OutlineNode( $scenario->getTitle(), $scenario->getTags(), $scenario->getSteps(), new ExampleTableNode($filteredTable, $scenario->getExampleTable()->getKeyword()), $scenario->getKeyword(), $scenario->getLine() ); } } $scenarios[] = $scenario; } return new FeatureNode( $feature->getTitle(), $feature->getDescription(), $feature->getTags(), $feature->getBackground(), $scenarios, $feature->getKeyword(), $feature->getLanguage(), $feature->getFile(), $feature->getLine() ); } } filterMinLine = intval($filterMinLine); if ($filterMaxLine == '*') { $this->filterMaxLine = PHP_INT_MAX; } else { $this->filterMaxLine = intval($filterMaxLine); } } public function isFeatureMatch(FeatureNode $feature) { return $this->filterMinLine <= $feature->getLine() && $this->filterMaxLine >= $feature->getLine(); } public function isScenarioMatch(ScenarioInterface $scenario) { if ($this->filterMinLine <= $scenario->getLine() && $this->filterMaxLine >= $scenario->getLine()) { return true; } if ($scenario instanceof OutlineNode && $scenario->hasExamples()) { foreach ($scenario->getExampleTable()->getLines() as $line) { if ($this->filterMinLine <= $line && $this->filterMaxLine >= $line) { return true; } } } return false; } public function filterFeature(FeatureNode $feature) { $scenarios = array(); foreach ($feature->getScenarios() as $scenario) { if (!$this->isScenarioMatch($scenario)) { continue; } if ($scenario instanceof OutlineNode && $scenario->hasExamples()) { $table = $scenario->getExampleTable()->getTable(); $lines = array_keys($table); $filteredTable = array($lines[0] => $table[$lines[0]]); unset($table[$lines[0]]); foreach ($table as $line => $row) { if ($this->filterMinLine <= $line && $this->filterMaxLine >= $line) { $filteredTable[$line] = $row; } } $scenario = new OutlineNode( $scenario->getTitle(), $scenario->getTags(), $scenario->getSteps(), new ExampleTableNode($filteredTable, $scenario->getExampleTable()->getKeyword()), $scenario->getKeyword(), $scenario->getLine() ); } $scenarios[] = $scenario; } return new FeatureNode( $feature->getTitle(), $feature->getDescription(), $feature->getTags(), $feature->getBackground(), $scenarios, $feature->getKeyword(), $feature->getLanguage(), $feature->getFile(), $feature->getLine() ); } } filterString = trim($filterString); } public function isFeatureMatch(FeatureNode $feature) { if ('/' === $this->filterString[0]) { return 1 === preg_match($this->filterString, $feature->getTitle()); } return false !== mb_strpos($feature->getTitle(), $this->filterString, 0, 'utf8'); } public function isScenarioMatch(ScenarioInterface $scenario) { if ('/' === $this->filterString[0] && 1 === preg_match($this->filterString, $scenario->getTitle())) { return true; } elseif (false !== mb_strpos($scenario->getTitle(), $this->filterString, 0, 'utf8')) { return true; } return false; } } regex = $regex; } public function isFeatureMatch(FeatureNode $feature) { return 1 === preg_match($this->regex, $feature->getDescription()); } public function isScenarioMatch(ScenarioInterface $scenario) { return false; } } filterPaths = array_map( function ($realpath) { return rtrim($realpath, DIRECTORY_SEPARATOR) . (is_dir($realpath) ? DIRECTORY_SEPARATOR : ''); }, array_filter( array_map('realpath', $paths) ) ); } public function isFeatureMatch(FeatureNode $feature) { foreach ($this->filterPaths as $path) { if (0 === strpos(realpath($feature->getFile()), $path)) { return true; } } return false; } public function isScenarioMatch(ScenarioInterface $scenario) { return false; } } pattern = '/as an? ' . strtr(preg_quote($role, '/'), array( '\*' => '.*', '\?' => '.', '\[' => '[', '\]' => ']' )) . '[$\n]/i'; } public function isFeatureMatch(FeatureNode $feature) { return 1 === preg_match($this->pattern, $feature->getDescription()); } public function isScenarioMatch(ScenarioInterface $scenario) { return false; } } isFeatureMatch($feature)) { return $feature; } $scenarios = array(); foreach ($feature->getScenarios() as $scenario) { if (!$this->isScenarioMatch($scenario)) { continue; } $scenarios[] = $scenario; } return new FeatureNode( $feature->getTitle(), $feature->getDescription(), $feature->getTags(), $feature->getBackground(), $scenarios, $feature->getKeyword(), $feature->getLanguage(), $feature->getFile(), $feature->getLine() ); } } filterString = trim($filterString); } public function isFeatureMatch(FeatureNode $feature) { return $this->isTagsMatchCondition($feature->getTags()); } public function isScenarioMatch(FeatureNode $feature, ScenarioInterface $scenario) { return $this->isTagsMatchCondition(array_merge($feature->getTags(), $scenario->getTags())); } protected function isTagsMatchCondition($tags) { $satisfies = true; foreach (explode('&&', $this->filterString) as $andTags) { $satisfiesComma = false; foreach (explode(',', $andTags) as $tag) { $tag = str_replace('@', '', trim($tag)); if ('~' === $tag[0]) { $tag = mb_substr($tag, 1, mb_strlen($tag, 'utf8') - 1, 'utf8'); $satisfiesComma = !in_array($tag, $tags) || $satisfiesComma; } else { $satisfiesComma = in_array($tag, $tags) || $satisfiesComma; } } $satisfies = (false !== $satisfiesComma && $satisfies && $satisfiesComma) || false; } return $satisfies; } } loaders[] = $loader; } public function addFilter(FeatureFilterInterface $filter) { $this->filters[] = $filter; } public function setFilters(array $filters) { $this->filters = array(); array_map(array($this, 'addFilter'), $filters); } public function setBasePath($path) { foreach ($this->loaders as $loader) { if ($loader instanceof FileLoaderInterface) { $loader->setBasePath($path); } } } public function load($resource, array $filters = array()) { $filters = array_merge($this->filters, $filters); $matches = array(); if (preg_match('/^(.*)\:(\d+)-(\d+|\*)$/', $resource, $matches)) { $resource = $matches[1]; $filters[] = new LineRangeFilter($matches[2], $matches[3]); } elseif (preg_match('/^(.*)\:(\d+)$/', $resource, $matches)) { $resource = $matches[1]; $filters[] = new LineFilter($matches[2]); } $loader = $this->resolveLoader($resource); if (null === $loader) { return array(); } $features = array(); foreach ($loader->load($resource) as $feature) { foreach ($filters as $filter) { $feature = $filter->filterFeature($feature); if (!$feature->hasScenarios() && !$filter->isFeatureMatch($feature)) { continue 2; } } $features[] = $feature; } return $features; } public function resolveLoader($resource) { foreach ($this->loaders as $loader) { if ($loader->supports($resource)) { return $loader; } } return null; } } keywords = $keywords; } public function setLanguage($language) { if (!isset($this->keywords[$language])) { $this->language = 'en'; } else { $this->language = $language; } } public function getFeatureKeywords() { return $this->keywords[$this->language]['feature']; } public function getBackgroundKeywords() { return $this->keywords[$this->language]['background']; } public function getScenarioKeywords() { return $this->keywords[$this->language]['scenario']; } public function getOutlineKeywords() { return $this->keywords[$this->language]['scenario_outline']; } public function getExamplesKeywords() { return $this->keywords[$this->language]['examples']; } public function getGivenKeywords() { return $this->keywords[$this->language]['given']; } public function getWhenKeywords() { return $this->keywords[$this->language]['when']; } public function getThenKeywords() { return $this->keywords[$this->language]['then']; } public function getAndKeywords() { return $this->keywords[$this->language]['and']; } public function getButKeywords() { return $this->keywords[$this->language]['but']; } public function getStepKeywords() { if (!isset($this->keywordString[$this->language])) { $keywords = array_merge( explode('|', $this->getGivenKeywords()), explode('|', $this->getWhenKeywords()), explode('|', $this->getThenKeywords()), explode('|', $this->getAndKeywords()), explode('|', $this->getButKeywords()) ); usort($keywords, function ($keyword1, $keyword2) { return mb_strlen($keyword2, 'utf8') - mb_strlen($keyword1, 'utf8'); }); $this->keywordString[$this->language] = implode('|', $keywords); } return $this->keywordString[$this->language]; } } setParsedFile($file); } throw $e; } parent::__construct($content); } public function getGivenKeywords() { return $this->prepareStepString(parent::getGivenKeywords()); } public function getWhenKeywords() { return $this->prepareStepString(parent::getWhenKeywords()); } public function getThenKeywords() { return $this->prepareStepString(parent::getThenKeywords()); } public function getAndKeywords() { return $this->prepareStepString(parent::getAndKeywords()); } public function getButKeywords() { return $this->prepareStepString(parent::getButKeywords()); } private function prepareStepString($keywordsString) { if (0 === mb_strpos($keywordsString, '*|', 0, 'UTF-8')) { $keywordsString = mb_substr($keywordsString, 2, mb_strlen($keywordsString, 'utf8') - 2, 'utf8'); } return $keywordsString; } } keywords = $keywords; $this->keywordsDumper = array($this, 'dumpKeywords'); } public function setKeywordsDumperFunction($mapper) { $this->keywordsDumper = $mapper; } public function dumpKeywords(array $keywords, $isShort) { if ($isShort) { return 1 < count($keywords) ? '(' . implode('|', $keywords) . ')' : $keywords[0]; } return $keywords[0]; } public function dump($language, $short = true, $excludeAsterisk = false) { $this->keywords->setLanguage($language); $languageComment = ''; if ('en' !== $language) { $languageComment = "# language: $language\n"; } $keywords = explode('|', $this->keywords->getFeatureKeywords()); if ($short) { $keywords = call_user_func($this->keywordsDumper, $keywords, $short); return trim($languageComment . $this->dumpFeature($keywords, $short, $excludeAsterisk)); } $features = array(); foreach ($keywords as $keyword) { $keyword = call_user_func($this->keywordsDumper, array($keyword), $short); $features[] = trim($languageComment . $this->dumpFeature($keyword, $short, $excludeAsterisk)); } return $features; } protected function dumpFeature($keyword, $short = true, $excludeAsterisk = false) { $dump = <<keywords->getBackgroundKeywords()); if ($short) { $keywords = call_user_func($this->keywordsDumper, $keywords, $short); $dump .= $this->dumpBackground($keywords, $short, $excludeAsterisk); } else { $keyword = call_user_func($this->keywordsDumper, array($keywords[0]), $short); $dump .= $this->dumpBackground($keyword, $short, $excludeAsterisk); } $keywords = explode('|', $this->keywords->getScenarioKeywords()); if ($short) { $keywords = call_user_func($this->keywordsDumper, $keywords, $short); $dump .= $this->dumpScenario($keywords, $short, $excludeAsterisk); } else { foreach ($keywords as $keyword) { $keyword = call_user_func($this->keywordsDumper, array($keyword), $short); $dump .= $this->dumpScenario($keyword, $short, $excludeAsterisk); } } $keywords = explode('|', $this->keywords->getOutlineKeywords()); if ($short) { $keywords = call_user_func($this->keywordsDumper, $keywords, $short); $dump .= $this->dumpOutline($keywords, $short, $excludeAsterisk); } else { foreach ($keywords as $keyword) { $keyword = call_user_func($this->keywordsDumper, array($keyword), $short); $dump .= $this->dumpOutline($keyword, $short, $excludeAsterisk); } } return $dump; } protected function dumpBackground($keyword, $short = true, $excludeAsterisk = false) { $dump = <<dumpStep( $this->keywords->getGivenKeywords(), 'there is agent A', $short, $excludeAsterisk ); $dump .= $this->dumpStep( $this->keywords->getAndKeywords(), 'there is agent B', $short, $excludeAsterisk ); return $dump . "\n"; } protected function dumpScenario($keyword, $short = true, $excludeAsterisk = false) { $dump = <<dumpStep( $this->keywords->getGivenKeywords(), 'there is agent J', $short, $excludeAsterisk ); $dump .= $this->dumpStep( $this->keywords->getAndKeywords(), 'there is agent K', $short, $excludeAsterisk ); $dump .= $this->dumpStep( $this->keywords->getWhenKeywords(), 'I erase agent K\'s memory', $short, $excludeAsterisk ); $dump .= $this->dumpStep( $this->keywords->getThenKeywords(), 'there should be agent J', $short, $excludeAsterisk ); $dump .= $this->dumpStep( $this->keywords->getButKeywords(), 'there should not be agent K', $short, $excludeAsterisk ); return $dump . "\n"; } protected function dumpOutline($keyword, $short = true, $excludeAsterisk = false) { $dump = <<dumpStep( $this->keywords->getGivenKeywords(), 'there is agent ', $short, $excludeAsterisk ); $dump .= $this->dumpStep( $this->keywords->getAndKeywords(), 'there is agent ', $short, $excludeAsterisk ); $dump .= $this->dumpStep( $this->keywords->getWhenKeywords(), 'I erase agent \'s memory', $short, $excludeAsterisk ); $dump .= $this->dumpStep( $this->keywords->getThenKeywords(), 'there should be agent ', $short, $excludeAsterisk ); $dump .= $this->dumpStep( $this->keywords->getButKeywords(), 'there should not be agent ', $short, $excludeAsterisk ); $keywords = explode('|', $this->keywords->getExamplesKeywords()); if ($short) { $keyword = call_user_func($this->keywordsDumper, $keywords, $short); } else { $keyword = call_user_func($this->keywordsDumper, array($keywords[0]), $short); } $dump .= <<keywordsDumper, $keywords, $short); $dump .= <<keywordsDumper, array($keyword), $short); $dump .= <<keywords = $keywords; } public function analyse($input, $language = 'en') { if ('UTF-8' !== mb_detect_encoding($input, 'UTF-8', true)) { throw new LexerException('Feature file is not in UTF8 encoding'); } $input = strtr($input, array("\r\n" => "\n", "\r" => "\n")); $this->lines = explode("\n", $input); $this->linesCount = count($this->lines); $this->line = $this->lines[0]; $this->lineNumber = 1; $this->trimmedLine = null; $this->eos = false; $this->deferredObjects = array(); $this->deferredObjectsCount = 0; $this->stashedToken = null; $this->inPyString = false; $this->pyStringSwallow = 0; $this->featureStarted = false; $this->allowMultilineArguments = false; $this->allowSteps = false; $this->keywords->setLanguage($this->language = $language); $this->keywordsCache = array(); $this->stepKeywordTypesCache = array(); } public function getLanguage() { return $this->language; } public function getAdvancedToken() { return $this->getStashedToken() ?: $this->getNextToken(); } public function deferToken(array $token) { $token['deferred'] = true; $this->deferredObjects[] = $token; ++$this->deferredObjectsCount; } public function predictToken() { if (null === $this->stashedToken) { $this->stashedToken = $this->getNextToken(); } return $this->stashedToken; } public function takeToken($type, $value = null) { return array( 'type' => $type, 'line' => $this->lineNumber, 'value' => $value ?: null, 'deferred' => false ); } protected function consumeLine() { ++$this->lineNumber; if (($this->lineNumber - 1) === $this->linesCount) { $this->eos = true; return; } $this->line = $this->lines[$this->lineNumber - 1]; $this->trimmedLine = null; } protected function getTrimmedLine() { return null !== $this->trimmedLine ? $this->trimmedLine : $this->trimmedLine = trim($this->line); } protected function getStashedToken() { $stashedToken = $this->stashedToken; $this->stashedToken = null; return $stashedToken; } protected function getDeferredToken() { if (!$this->deferredObjectsCount) { return null; } --$this->deferredObjectsCount; return array_shift($this->deferredObjects); } protected function getNextToken() { return $this->getDeferredToken() ?: $this->scanEOS() ?: $this->scanLanguage() ?: $this->scanComment() ?: $this->scanPyStringOp() ?: $this->scanPyStringContent() ?: $this->scanStep() ?: $this->scanScenario() ?: $this->scanBackground() ?: $this->scanOutline() ?: $this->scanExamples() ?: $this->scanFeature() ?: $this->scanTags() ?: $this->scanTableRow() ?: $this->scanNewline() ?: $this->scanText(); } protected function scanInput($regex, $type) { if (!preg_match($regex, $this->line, $matches)) { return null; } $token = $this->takeToken($type, $matches[1]); $this->consumeLine(); return $token; } protected function scanInputForKeywords($keywords, $type) { if (!preg_match('/^(\s*)(' . $keywords . '):\s*(.*)/u', $this->line, $matches)) { return null; } $token = $this->takeToken($type, $matches[3]); $token['keyword'] = $matches[2]; $token['indent'] = mb_strlen($matches[1], 'utf8'); $this->consumeLine(); if ('Feature' === $type) { $this->featureStarted = true; } if ('Feature' === $type || 'Scenario' === $type || 'Outline' === $type) { $this->allowMultilineArguments = false; } elseif ('Examples' === $type) { $this->allowMultilineArguments = true; } if ('Scenario' === $type || 'Background' === $type || 'Outline' === $type) { $this->allowSteps = true; } return $token; } protected function scanEOS() { if (!$this->eos) { return null; } return $this->takeToken('EOS'); } protected function getKeywords($type) { if (!isset($this->keywordsCache[$type])) { $getter = 'get' . $type . 'Keywords'; $keywords = $this->keywords->$getter(); if ('Step' === $type) { $padded = array(); foreach (explode('|', $keywords) as $keyword) { $padded[] = false !== mb_strpos($keyword, '<', 0, 'utf8') ? preg_quote(mb_substr($keyword, 0, -1, 'utf8'), '/') . '\s*' : preg_quote($keyword, '/') . '\s+'; } $keywords = implode('|', $padded); } $this->keywordsCache[$type] = $keywords; } return $this->keywordsCache[$type]; } protected function scanFeature() { return $this->scanInputForKeywords($this->getKeywords('Feature'), 'Feature'); } protected function scanBackground() { return $this->scanInputForKeywords($this->getKeywords('Background'), 'Background'); } protected function scanScenario() { return $this->scanInputForKeywords($this->getKeywords('Scenario'), 'Scenario'); } protected function scanOutline() { return $this->scanInputForKeywords($this->getKeywords('Outline'), 'Outline'); } protected function scanExamples() { return $this->scanInputForKeywords($this->getKeywords('Examples'), 'Examples'); } protected function scanStep() { if (!$this->allowSteps) { return null; } $keywords = $this->getKeywords('Step'); if (!preg_match('/^\s*(' . $keywords . ')([^\s].+)/u', $this->line, $matches)) { return null; } $keyword = trim($matches[1]); $token = $this->takeToken('Step', $keyword); $token['keyword_type'] = $this->getStepKeywordType($keyword); $token['text'] = $matches[2]; $this->consumeLine(); $this->allowMultilineArguments = true; return $token; } protected function scanPyStringOp() { if (!$this->allowMultilineArguments) { return null; } if (false === ($pos = mb_strpos($this->line, '"""', 0, 'utf8'))) { return null; } $this->inPyString = !$this->inPyString; $token = $this->takeToken('PyStringOp'); $this->pyStringSwallow = $pos; $this->consumeLine(); return $token; } protected function scanPyStringContent() { if (!$this->inPyString) { return null; } $token = $this->scanText(); $token['value'] = preg_replace('/^\s{0,' . $this->pyStringSwallow . '}/u', '', $token['value']); return $token; } protected function scanTableRow() { if (!$this->allowMultilineArguments) { return null; } $line = $this->getTrimmedLine(); if (!isset($line[0]) || '|' !== $line[0] || '|' !== substr($line, -1)) { return null; } $token = $this->takeToken('TableRow'); $line = mb_substr($line, 1, mb_strlen($line, 'utf8') - 2, 'utf8'); $columns = array_map(function ($column) { return trim(str_replace('\\|', '|', $column)); }, preg_split('/(?consumeLine(); return $token; } protected function scanTags() { $line = $this->getTrimmedLine(); if (!isset($line[0]) || '@' !== $line[0]) { return null; } $token = $this->takeToken('Tag'); $tags = explode('@', mb_substr($line, 1, mb_strlen($line, 'utf8') - 1, 'utf8')); $tags = array_map('trim', $tags); $token['tags'] = $tags; $this->consumeLine(); return $token; } protected function scanLanguage() { if ($this->featureStarted) { return null; } if ($this->inPyString) { return null; } if (0 !== mb_strpos(ltrim($this->line), '#', 0, 'utf8')) { return null; } return $this->scanInput('/^\s*\#\s*language:\s*([\w_\-]+)\s*$/', 'Language'); } protected function scanComment() { if ($this->inPyString) { return null; } $line = $this->getTrimmedLine(); if (0 !== mb_strpos($line, '#', 0, 'utf8')) { return null; } $token = $this->takeToken('Comment', $line); $this->consumeLine(); return $token; } protected function scanNewline() { if ('' !== $this->getTrimmedLine()) { return null; } $token = $this->takeToken('Newline', mb_strlen($this->line, 'utf8')); $this->consumeLine(); return $token; } protected function scanText() { $token = $this->takeToken('Text', $this->line); $this->consumeLine(); return $token; } private function getStepKeywordType($native) { if ('*' === $native) { return 'And'; } if (empty($this->stepKeywordTypesCache)) { $this->stepKeywordTypesCache = array( 'Given' => explode('|', $this->keywords->getGivenKeywords()), 'When' => explode('|', $this->keywords->getWhenKeywords()), 'Then' => explode('|', $this->keywords->getThenKeywords()), 'And' => explode('|', $this->keywords->getAndKeywords()), 'But' => explode('|', $this->keywords->getButKeywords()) ); } foreach ($this->stepKeywordTypesCache as $type => $keywords) { if (in_array($native, $keywords) || in_array($native . '<', $keywords)) { return $type; } } return 'Given'; } } basePath = realpath($path); } protected function findRelativePath($path) { if (null !== $this->basePath) { return strtr($path, array($this->basePath . DIRECTORY_SEPARATOR => '')); } return $path; } protected function findAbsolutePath($path) { if (is_file($path) || is_dir($path)) { return realpath($path); } if (null === $this->basePath) { return false; } if (is_file($this->basePath . DIRECTORY_SEPARATOR . $path) || is_dir($this->basePath . DIRECTORY_SEPARATOR . $path)) { return realpath($this->basePath . DIRECTORY_SEPARATOR . $path); } return false; } } $hash) { $feature = $this->loadFeatureHash($hash, $iterator); $features[] = $feature; } } elseif (isset($resource['feature'])) { $feature = $this->loadFeatureHash($resource['feature']); $features[] = $feature; } return $features; } protected function loadFeatureHash(array $hash, $line = 0) { $hash = array_merge( array( 'title' => null, 'description' => null, 'tags' => array(), 'keyword' => 'Feature', 'language' => 'en', 'line' => $line, 'scenarios' => array(), ), $hash ); $background = isset($hash['background']) ? $this->loadBackgroundHash($hash['background']) : null; $scenarios = array(); foreach ((array) $hash['scenarios'] as $scenarioIterator => $scenarioHash) { if (isset($scenarioHash['type']) && 'outline' === $scenarioHash['type']) { $scenarios[] = $this->loadOutlineHash($scenarioHash, $scenarioIterator); } else { $scenarios[] = $this->loadScenarioHash($scenarioHash, $scenarioIterator); } } return new FeatureNode($hash['title'], $hash['description'], $hash['tags'], $background, $scenarios, $hash['keyword'], $hash['language'], null, $hash['line']); } protected function loadBackgroundHash(array $hash) { $hash = array_merge( array( 'title' => null, 'keyword' => 'Background', 'line' => 0, 'steps' => array(), ), $hash ); $steps = $this->loadStepsHash($hash['steps']); return new BackgroundNode($hash['title'], $steps, $hash['keyword'], $hash['line']); } protected function loadScenarioHash(array $hash, $line = 0) { $hash = array_merge( array( 'title' => null, 'tags' => array(), 'keyword' => 'Scenario', 'line' => $line, 'steps' => array(), ), $hash ); $steps = $this->loadStepsHash($hash['steps']); return new ScenarioNode($hash['title'], $hash['tags'], $steps, $hash['keyword'], $hash['line']); } protected function loadOutlineHash(array $hash, $line = 0) { $hash = array_merge( array( 'title' => null, 'tags' => array(), 'keyword' => 'Scenario Outline', 'line' => $line, 'steps' => array(), 'examples' => array(), ), $hash ); $steps = $this->loadStepsHash($hash['steps']); if (isset($hash['examples']['keyword'])) { $examplesKeyword = $hash['examples']['keyword']; unset($hash['examples']['keyword']); } else { $examplesKeyword = 'Examples'; } $examples = new ExampleTableNode($hash['examples'], $examplesKeyword); return new OutlineNode($hash['title'], $hash['tags'], $steps, $examples, $hash['keyword'], $hash['line']); } private function loadStepsHash(array $hash) { $steps = array(); foreach ($hash as $stepIterator => $stepHash) { $steps[] = $this->loadStepHash($stepHash, $stepIterator); } return $steps; } protected function loadStepHash(array $hash, $line = 0) { $hash = array_merge( array( 'keyword_type' => 'Given', 'type' => 'Given', 'text' => null, 'keyword' => 'Scenario', 'line' => $line, 'arguments' => array(), ), $hash ); $arguments = array(); foreach ($hash['arguments'] as $argumentHash) { if ('table' === $argumentHash['type']) { $arguments[] = $this->loadTableHash($argumentHash['rows']); } elseif ('pystring' === $argumentHash['type']) { $arguments[] = $this->loadPyStringHash($argumentHash, $hash['line'] + 1); } } return new StepNode($hash['type'], $hash['text'], $arguments, $hash['line'], $hash['keyword_type']); } protected function loadTableHash(array $hash) { return new TableNode($hash); } protected function loadPyStringHash(array $hash, $line = 0) { $line = isset($hash['line']) ? $hash['line'] : $line; $strings = array(); foreach (explode("\n", $hash['text']) as $string) { $strings[] = $string; } return new PyStringNode($strings, $line); } } gherkin = $gherkin; } public function supports($path) { return is_string($path) && is_dir($this->findAbsolutePath($path)); } public function load($path) { $path = $this->findAbsolutePath($path); $iterator = new RecursiveIteratorIterator( new RecursiveDirectoryIterator($path, RecursiveDirectoryIterator::SKIP_DOTS) ); $paths = array_map('strval', iterator_to_array($iterator)); uasort($paths, 'strnatcasecmp'); $features = array(); foreach ($paths as $path) { $path = (string) $path; $loader = $this->gherkin->resolveLoader($path); if (null !== $loader) { $features = array_merge($features, $loader->load($path)); } } return $features; } } parser = $parser; $this->cache = $cache; } public function setCache(CacheInterface $cache) { $this->cache = $cache; } public function supports($path) { return is_string($path) && is_file($absolute = $this->findAbsolutePath($path)) && 'feature' === pathinfo($absolute, PATHINFO_EXTENSION); } public function load($path) { $path = $this->findAbsolutePath($path); if ($this->cache) { if ($this->cache->isFresh($path, filemtime($path))) { $feature = $this->cache->read($path); } elseif (null !== $feature = $this->parseFeature($path)) { $this->cache->write($path, $feature); } } else { $feature = $this->parseFeature($path); } return null !== $feature ? array($feature) : array(); } protected function parseFeature($path) { $filename = $this->findRelativePath($path); $content = file_get_contents($path); $feature = $this->parser->parse($content, $filename); return $feature; } } loader = new ArrayLoader(); } public function supports($path) { return is_string($path) && is_file($absolute = $this->findAbsolutePath($path)) && 'yml' === pathinfo($absolute, PATHINFO_EXTENSION); } public function load($path) { $path = $this->findAbsolutePath($path); $hash = Yaml::parse(file_get_contents($path)); $features = $this->loader->load($hash); $filename = $this->findRelativePath($path); return array_map(function (FeatureNode $feature) use ($filename) { return new FeatureNode( $feature->getTitle(), $feature->getDescription(), $feature->getTags(), $feature->getBackground(), $feature->getScenarios(), $feature->getKeyword(), $feature->getLanguage(), $filename, $feature->getLine() ); }, $features); } } title = $title; $this->steps = $steps; $this->keyword = $keyword; $this->line = $line; } public function getNodeType() { return 'Background'; } public function getTitle() { return $this->title; } public function hasSteps() { return 0 < count($this->steps); } public function getSteps() { return $this->steps; } public function getKeyword() { return $this->keyword; } public function getLine() { return $this->line; } } title = $title; $this->tags = $tags; $this->outlineSteps = $outlineSteps; $this->tokens = $tokens; $this->line = $line; $this->outlineTitle = $outlineTitle; } public function getNodeType() { return 'Example'; } public function getKeyword() { return $this->getNodeType(); } public function getTitle() { return $this->title; } public function hasTag($tag) { return in_array($tag, $this->getTags()); } public function hasTags() { return 0 < count($this->getTags()); } public function getTags() { return $this->tags; } public function hasSteps() { return 0 < count($this->outlineSteps); } public function getSteps() { return $this->steps = $this->steps ? : $this->createExampleSteps(); } public function getTokens() { return $this->tokens; } public function getLine() { return $this->line; } public function getOutlineTitle() { return $this->outlineTitle; } protected function createExampleSteps() { $steps = array(); foreach ($this->outlineSteps as $outlineStep) { $keyword = $outlineStep->getKeyword(); $keywordType = $outlineStep->getKeywordType(); $text = $this->replaceTextTokens($outlineStep->getText()); $args = $this->replaceArgumentsTokens($outlineStep->getArguments()); $line = $outlineStep->getLine(); $steps[] = new StepNode($keyword, $text, $args, $line, $keywordType); } return $steps; } protected function replaceArgumentsTokens(array $arguments) { foreach ($arguments as $num => $argument) { if ($argument instanceof TableNode) { $arguments[$num] = $this->replaceTableArgumentTokens($argument); } if ($argument instanceof PyStringNode) { $arguments[$num] = $this->replacePyStringArgumentTokens($argument); } } return $arguments; } protected function replaceTableArgumentTokens(TableNode $argument) { $table = $argument->getTable(); foreach ($table as $line => $row) { foreach (array_keys($row) as $col) { $table[$line][$col] = $this->replaceTextTokens($table[$line][$col]); } } return new TableNode($table); } protected function replacePyStringArgumentTokens(PyStringNode $argument) { $strings = $argument->getStrings(); foreach ($strings as $line => $string) { $strings[$line] = $this->replaceTextTokens($strings[$line]); } return new PyStringNode($strings, $argument->getLine()); } protected function replaceTextTokens($text) { foreach ($this->tokens as $key => $val) { $text = str_replace('<' . $key . '>', $val, $text); } return $text; } } keyword = $keyword; parent::__construct($table); } public function getNodeType() { return 'ExampleTable'; } public function getKeyword() { return $this->keyword; } } title = $title; $this->description = $description; $this->tags = $tags; $this->background = $background; $this->scenarios = $scenarios; $this->keyword = $keyword; $this->language = $language; $this->file = $file; $this->line = $line; } public function getNodeType() { return 'Feature'; } public function getTitle() { return $this->title; } public function hasDescription() { return !empty($this->description); } public function getDescription() { return $this->description; } public function hasTag($tag) { return in_array($tag, $this->tags); } public function hasTags() { return 0 < count($this->tags); } public function getTags() { return $this->tags; } public function hasBackground() { return null !== $this->background; } public function getBackground() { return $this->background; } public function hasScenarios() { return 0 < count($this->scenarios); } public function getScenarios() { return $this->scenarios; } public function getKeyword() { return $this->keyword; } public function getLanguage() { return $this->language; } public function getFile() { return $this->file; } public function getLine() { return $this->line; } } title = $title; $this->tags = $tags; $this->steps = $steps; $this->table = $table; $this->keyword = $keyword; $this->line = $line; } public function getNodeType() { return 'Outline'; } public function getTitle() { return $this->title; } public function hasTag($tag) { return in_array($tag, $this->getTags()); } public function hasTags() { return 0 < count($this->getTags()); } public function getTags() { return $this->tags; } public function hasSteps() { return 0 < count($this->steps); } public function getSteps() { return $this->steps; } public function hasExamples() { return 0 < count($this->table->getColumnsHash()); } public function getExampleTable() { return $this->table; } public function getExamples() { return $this->examples = $this->examples ? : $this->createExamples(); } public function getKeyword() { return $this->keyword; } public function getLine() { return $this->line; } protected function createExamples() { $examples = array(); foreach ($this->table->getColumnsHash() as $rowNum => $row) { $examples[] = new ExampleNode( $this->table->getRowAsString($rowNum + 1), $this->tags, $this->getSteps(), $row, $this->table->getRowLine($rowNum + 1), $this->getTitle() ); } return $examples; } } strings = $strings; $this->line = $line; } public function getNodeType() { return 'PyString'; } public function getStrings() { return $this->strings; } public function getRaw() { return implode("\n", $this->strings); } public function __toString() { return $this->getRaw(); } public function getLine() { return $this->line; } } title = $title; $this->tags = $tags; $this->steps = $steps; $this->keyword = $keyword; $this->line = $line; } public function getNodeType() { return 'Scenario'; } public function getTitle() { return $this->title; } public function hasTag($tag) { return in_array($tag, $this->getTags()); } public function hasTags() { return 0 < count($this->getTags()); } public function getTags() { return $this->tags; } public function hasSteps() { return 0 < count($this->steps); } public function getSteps() { return $this->steps; } public function getKeyword() { return $this->keyword; } public function getLine() { return $this->line; } } 1) { throw new NodeException(sprintf( 'Steps could have only one argument, but `%s %s` have %d.', $keyword, $text, count($arguments) )); } $this->keyword = $keyword; $this->text = $text; $this->arguments = $arguments; $this->line = $line; $this->keywordType = $keywordType ?: 'Given'; } public function getNodeType() { return 'Step'; } public function getType() { return $this->getKeyword(); } public function getKeyword() { return $this->keyword; } public function getKeywordType() { return $this->keywordType; } public function getText() { return $this->text; } public function hasArguments() { return 0 < count($this->arguments); } public function getArguments() { return $this->arguments; } public function getLine() { return $this->line; } } table = $table; $columnCount = null; foreach ($this->getRows() as $row) { if (!is_array($row)) { throw new NodeException('Table is not two-dimensional.'); } if ($columnCount === null) { $columnCount = count($row); } if (count($row) !== $columnCount) { throw new NodeException('Table does not have same number of columns in every row.'); } if (!is_array($row)) { throw new NodeException('Table is not two-dimensional.'); } foreach ($row as $column => $string) { if (!isset($this->maxLineLength[$column])) { $this->maxLineLength[$column] = 0; } if (!is_scalar($string)) { throw new NodeException('Table is not two-dimensional.'); } $this->maxLineLength[$column] = max($this->maxLineLength[$column], mb_strlen($string, 'utf8')); } } } public static function fromList(array $list) { if (count($list) !== count($list, COUNT_RECURSIVE)) { throw new NodeException('List is not a one-dimensional array.'); } array_walk($list, function (&$item) { $item = array($item); }); return new self($list); } public function getNodeType() { return 'Table'; } public function getHash() { return $this->getColumnsHash(); } public function getColumnsHash() { $rows = $this->getRows(); $keys = array_shift($rows); $hash = array(); foreach ($rows as $row) { $hash[] = array_combine($keys, $row); } return $hash; } public function getRowsHash() { $hash = array(); foreach ($this->getRows() as $row) { $hash[array_shift($row)] = (1 == count($row)) ? $row[0] : $row; } return $hash; } public function getTable() { return $this->table; } public function getRows() { return array_values($this->table); } public function getLines() { return array_keys($this->table); } public function getRow($index) { $rows = $this->getRows(); if (!isset($rows[$index])) { throw new NodeException(sprintf('Rows #%d does not exist in table.', $index)); } return $rows[$index]; } public function getColumn($index) { if ($index >= count($this->getRow(0))) { throw new NodeException(sprintf('Column #%d does not exist in table.', $index)); } $rows = $this->getRows(); $column = array(); foreach ($rows as $row) { $column[] = $row[$index]; } return $column; } public function getRowLine($index) { $lines = array_keys($this->table); if (!isset($lines[$index])) { throw new NodeException(sprintf('Rows #%d does not exist in table.', $index)); } return $lines[$index]; } public function getRowAsString($rowNum) { $values = array(); foreach ($this->getRow($rowNum) as $column => $value) { $values[] = $this->padRight(' ' . $value . ' ', $this->maxLineLength[$column] + 2); } return sprintf('|%s|', implode('|', $values)); } public function getRowAsStringWithWrappedValues($rowNum, $wrapper) { $values = array(); foreach ($this->getRow($rowNum) as $column => $value) { $value = $this->padRight(' ' . $value . ' ', $this->maxLineLength[$column] + 2); $values[] = call_user_func($wrapper, $value, $column); } return sprintf('|%s|', implode('|', $values)); } public function getTableAsString() { $lines = array(); for ($i = 0; $i < count($this->getRows()); $i++) { $lines[] = $this->getRowAsString($i); } return implode("\n", $lines); } public function getLine() { return $this->getRowLine(0); } public function __toString() { return $this->getTableAsString(); } public function getIterator() { return new ArrayIterator($this->getHash()); } protected function padRight($text, $length) { while ($length > mb_strlen($text, 'utf8')) { $text = $text . ' '; } return $text; } } lexer = $lexer; } public function parse($input, $file = null) { $this->languageSpecifierLine = null; $this->input = $input; $this->file = $file; $this->tags = array(); try { $this->lexer->analyse($this->input, 'en'); } catch (LexerException $e) { throw new ParserException( sprintf('Lexer exception "%s" thrown for file %s', $e->getMessage(), $file), 0, $e ); } $feature = null; while ('EOS' !== ($predicted = $this->predictTokenType())) { $node = $this->parseExpression(); if (null === $node || "\n" === $node) { continue; } if (!$feature && $node instanceof FeatureNode) { $feature = $node; continue; } if ($feature && $node instanceof FeatureNode) { throw new ParserException(sprintf( 'Only one feature is allowed per feature file. But %s got multiple.', $this->file )); } if (is_string($node)) { throw new ParserException(sprintf( 'Expected Feature, but got text: "%s"%s', $node, $this->file ? ' in file: ' . $this->file : '' )); } if (!$node instanceof FeatureNode) { throw new ParserException(sprintf( 'Expected Feature, but got %s on line: %d%s', $node->getKeyword(), $node->getLine(), $this->file ? ' in file: ' . $this->file : '' )); } } return $feature; } protected function expectTokenType($type) { $types = (array) $type; if (in_array($this->predictTokenType(), $types)) { return $this->lexer->getAdvancedToken(); } $token = $this->lexer->predictToken(); throw new ParserException(sprintf( 'Expected %s token, but got %s on line: %d%s', implode(' or ', $types), $this->predictTokenType(), $token['line'], $this->file ? ' in file: ' . $this->file : '' )); } protected function acceptTokenType($type) { if ($type !== $this->predictTokenType()) { return null; } return $this->lexer->getAdvancedToken(); } protected function predictTokenType() { $token = $this->lexer->predictToken(); return $token['type']; } protected function parseExpression() { switch ($type = $this->predictTokenType()) { case 'Feature': return $this->parseFeature(); case 'Background': return $this->parseBackground(); case 'Scenario': return $this->parseScenario(); case 'Outline': return $this->parseOutline(); case 'Examples': return $this->parseExamples(); case 'TableRow': return $this->parseTable(); case 'PyStringOp': return $this->parsePyString(); case 'Step': return $this->parseStep(); case 'Text': return $this->parseText(); case 'Newline': return $this->parseNewline(); case 'Tag': return $this->parseTags(); case 'Comment': return $this->parseComment(); case 'Language': return $this->parseLanguage(); case 'EOS': return ''; } throw new ParserException(sprintf('Unknown token type: %s', $type)); } protected function parseFeature() { $token = $this->expectTokenType('Feature'); $title = trim($token['value']) ?: null; $description = null; $tags = $this->popTags(); $background = null; $scenarios = array(); $keyword = $token['keyword']; $language = $this->lexer->getLanguage(); $file = $this->file; $line = $token['line']; while ('EOS' !== $this->predictTokenType()) { $node = $this->parseExpression(); if (is_string($node)) { $text = preg_replace('/^\s{0,' . ($token['indent'] + 2) . '}|\s*$/', '', $node); $description .= (null !== $description ? "\n" : '') . $text; continue; } if (!$background && $node instanceof BackgroundNode) { $background = $node; continue; } if ($node instanceof ScenarioInterface) { $scenarios[] = $node; continue; } if ($background instanceof BackgroundNode && $node instanceof BackgroundNode) { throw new ParserException(sprintf( 'Each Feature could have only one Background, but found multiple on lines %d and %d%s', $background->getLine(), $node->getLine(), $this->file ? ' in file: ' . $this->file : '' )); } if (!$node instanceof ScenarioNode) { throw new ParserException(sprintf( 'Expected Scenario, Outline or Background, but got %s on line: %d%s', $node->getNodeType(), $node->getLine(), $this->file ? ' in file: ' . $this->file : '' )); } } return new FeatureNode( rtrim($title) ?: null, rtrim($description) ?: null, $tags, $background, $scenarios, $keyword, $language, $file, $line ); } protected function parseBackground() { $token = $this->expectTokenType('Background'); $title = trim($token['value']); $keyword = $token['keyword']; $line = $token['line']; if (count($this->popTags())) { throw new ParserException(sprintf( 'Background can not be tagged, but it is on line: %d%s', $line, $this->file ? ' in file: ' . $this->file : '' )); } $steps = array(); $allowedTokenTypes = array('Step', 'Newline', 'Text', 'Comment'); while (in_array($this->predictTokenType(), $allowedTokenTypes)) { $node = $this->parseExpression(); if ($node instanceof StepNode) { $steps[] = $this->normalizeStepNodeKeywordType($node, $steps); continue; } if (!count($steps) && is_string($node)) { $text = preg_replace('/^\s{0,' . ($token['indent'] + 2) . '}|\s*$/', '', $node); $title .= "\n" . $text; continue; } if ("\n" === $node) { continue; } if (is_string($node)) { throw new ParserException(sprintf( 'Expected Step, but got text: "%s"%s', $node, $this->file ? ' in file: ' . $this->file : '' )); } if (!$node instanceof StepNode) { throw new ParserException(sprintf( 'Expected Step, but got %s on line: %d%s', $node->getNodeType(), $node->getLine(), $this->file ? ' in file: ' . $this->file : '' )); } } return new BackgroundNode(rtrim($title) ?: null, $steps, $keyword, $line); } protected function parseScenario() { $token = $this->expectTokenType('Scenario'); $title = trim($token['value']); $tags = $this->popTags(); $keyword = $token['keyword']; $line = $token['line']; $steps = array(); while (in_array($this->predictTokenType(), array('Step', 'Newline', 'Text', 'Comment'))) { $node = $this->parseExpression(); if ($node instanceof StepNode) { $steps[] = $this->normalizeStepNodeKeywordType($node, $steps); continue; } if (!count($steps) && is_string($node)) { $text = preg_replace('/^\s{0,' . ($token['indent'] + 2) . '}|\s*$/', '', $node); $title .= "\n" . $text; continue; } if ("\n" === $node) { continue; } if (is_string($node)) { throw new ParserException(sprintf( 'Expected Step, but got text: "%s"%s', $node, $this->file ? ' in file: ' . $this->file : '' )); } if (!$node instanceof StepNode) { throw new ParserException(sprintf( 'Expected Step, but got %s on line: %d%s', $node->getNodeType(), $node->getLine(), $this->file ? ' in file: ' . $this->file : '' )); } } return new ScenarioNode(rtrim($title) ?: null, $tags, $steps, $keyword, $line); } protected function parseOutline() { $token = $this->expectTokenType('Outline'); $title = trim($token['value']); $tags = $this->popTags(); $keyword = $token['keyword']; $examples = null; $line = $token['line']; $steps = array(); while (in_array($this->predictTokenType(), array('Step', 'Examples', 'Newline', 'Text', 'Comment'))) { $node = $this->parseExpression(); if ($node instanceof StepNode) { $steps[] = $this->normalizeStepNodeKeywordType($node, $steps); continue; } if ($node instanceof ExampleTableNode) { $examples = $node; continue; } if (!count($steps) && is_string($node)) { $text = preg_replace('/^\s{0,' . ($token['indent'] + 2) . '}|\s*$/', '', $node); $title .= "\n" . $text; continue; } if ("\n" === $node) { continue; } if (is_string($node)) { throw new ParserException(sprintf( 'Expected Step or Examples table, but got text: "%s"%s', $node, $this->file ? ' in file: ' . $this->file : '' )); } if (!$node instanceof StepNode) { throw new ParserException(sprintf( 'Expected Step or Examples table, but got %s on line: %d%s', $node->getNodeType(), $node->getLine(), $this->file ? ' in file: ' . $this->file : '' )); } } if (null === $examples) { throw new ParserException(sprintf( 'Outline should have examples table, but got none for outline "%s" on line: %d%s', rtrim($title), $line, $this->file ? ' in file: ' . $this->file : '' )); } return new OutlineNode(rtrim($title) ?: null, $tags, $steps, $examples, $keyword, $line); } protected function parseStep() { $token = $this->expectTokenType('Step'); $keyword = $token['value']; $keywordType = $token['keyword_type']; $text = trim($token['text']); $line = $token['line']; $arguments = array(); while (in_array($predicted = $this->predictTokenType(), array('PyStringOp', 'TableRow', 'Newline', 'Comment'))) { if ('Comment' === $predicted || 'Newline' === $predicted) { $this->acceptTokenType($predicted); continue; } $node = $this->parseExpression(); if ($node instanceof PyStringNode || $node instanceof TableNode) { $arguments[] = $node; } } return new StepNode($keyword, $text, $arguments, $line, $keywordType); } protected function parseExamples() { $token = $this->expectTokenType('Examples'); $keyword = $token['keyword']; return new ExampleTableNode($this->parseTableRows(), $keyword); } protected function parseTable() { return new TableNode($this->parseTableRows()); } protected function parsePyString() { $token = $this->expectTokenType('PyStringOp'); $line = $token['line']; $strings = array(); while ('PyStringOp' !== ($predicted = $this->predictTokenType()) && 'Text' === $predicted) { $token = $this->expectTokenType('Text'); $strings[] = $token['value']; } $this->expectTokenType('PyStringOp'); return new PyStringNode($strings, $line); } protected function parseTags() { $token = $this->expectTokenType('Tag'); $this->tags = array_merge($this->tags, $token['tags']); return $this->parseExpression(); } protected function popTags() { $tags = $this->tags; $this->tags = array(); return $tags; } protected function parseText() { $token = $this->expectTokenType('Text'); return $token['value']; } protected function parseNewline() { $this->expectTokenType('Newline'); return "\n"; } protected function parseComment() { $this->expectTokenType('Comment'); return $this->parseExpression(); } protected function parseLanguage() { $token = $this->expectTokenType('Language'); if (null === $this->languageSpecifierLine) { $this->lexer->analyse($this->input, $token['value']); $this->languageSpecifierLine = $token['line']; } elseif ($token['line'] !== $this->languageSpecifierLine) { throw new ParserException(sprintf( 'Ambiguous language specifiers on lines: %d and %d%s', $this->languageSpecifierLine, $token['line'], $this->file ? ' in file: ' . $this->file : '' )); } return $this->parseExpression(); } private function parseTableRows() { $table = array(); while (in_array($predicted = $this->predictTokenType(), array('TableRow', 'Newline', 'Comment'))) { if ('Comment' === $predicted || 'Newline' === $predicted) { $this->acceptTokenType($predicted); continue; } $token = $this->expectTokenType('TableRow'); $table[$token['line']] = $token['columns']; } return $table; } private function normalizeStepNodeKeywordType(StepNode $node, array $steps = array()) { if (in_array($node->getKeywordType(), array('And', 'But'))) { if (($prev = end($steps))) { $keywordType = $prev->getKeywordType(); } else { $keywordType = 'Given'; } $node = new StepNode( $node->getKeyword(), $node->getText(), $node->getArguments(), $node->getLine(), $keywordType ); } return $node; } } '*']; file_put_contents(__DIR__ . '/composer.json', json_encode($config)); } public function test($params) { return $this->taskExec(__DIR__ . '/vendor/bin/codecept run ' . $params) ->dir(__DIR__ .'/vendor/codeception/codeception') ->run(); } }count()) { return false; } if ($this->string === '') { return true; } foreach ($nodes as $node) { if (parent::matches($node->nodeValue)) { return true; } } return false; } protected function fail($nodes, $selector, ComparisonFailure $comparisonFailure = null) { if (!$nodes->count()) { throw new ElementNotFound($selector, 'Element located either by name, CSS or XPath'); } $output = "Failed asserting that any element by '$selector'"; $output .= $this->uriMessage('on page'); $output .= " "; if ($nodes->count() < 10) { $output .= $this->nodesList($nodes); } else { $message = new Message("[total %s elements]"); $output .= $message->with($nodes->count())->getMessage(); } $output .= "\ncontains text '{$this->string}'"; throw new \PHPUnit\Framework\ExpectationFailedException( $output, $comparisonFailure ); } protected function failureDescription($other) { $desc = ''; foreach ($other as $o) { $desc .= parent::failureDescription($o->textContent); } return $desc; } protected function nodesList(DomCrawler $nodes, $contains = null) { $output = ""; foreach ($nodes as $node) { if ($contains && strpos($node->nodeValue, $contains) === false) { continue; } $output .= "\n+ " . $node->C14N(); } return $output; } } string) { throw new \PHPUnit\Framework\ExpectationFailedException( "Element '$selector' was found", $comparisonFailure ); } $output = "There was '$selector' element"; $output .= $this->uriMessage('on page'); $output .= $this->nodesList($nodes, $this->string); $output .= "\ncontaining '{$this->string}'"; throw new \PHPUnit\Framework\ExpectationFailedException( $output, $comparisonFailure ); } public function toString() { if ($this->string) { return 'that contains text "' . $this->string . '"'; } } } expected = $expected; } protected function matches($other) { $jsonResponseArray = new JsonArray($other); if (!is_array($jsonResponseArray->toArray())) { throw new \PHPUnit\Framework\AssertionFailedError('JSON response is not an array: ' . $other); } if ($jsonResponseArray->containsArray($this->expected)) { return true; } $comparator = new ArrayComparator(); $comparator->setFactory(new Factory); try { $comparator->assertEquals($this->expected, $jsonResponseArray->toArray()); } catch (ComparisonFailure $failure) { throw new \PHPUnit\Framework\ExpectationFailedException( "Response JSON does not contain the provided JSON\n", $failure ); } } public function toString() { return ''; } protected function failureDescription($other) { return ''; } } jsonType = $jsonType; $this->match = $match; } protected function matches($jsonArray) { if ($jsonArray instanceof JsonArray) { $jsonArray = $jsonArray->toArray(); } $matched = (new JsonTypeUtil($jsonArray))->matches($this->jsonType); if ($this->match) { if ($matched !== true) { throw new \PHPUnit\Framework\ExpectationFailedException($matched); } } else { if ($matched === true) { throw new \PHPUnit\Framework\ExpectationFailedException('Unexpectedly response matched: ' . json_encode($jsonArray)); } } return true; } public function toString() { return ''; } protected function failureDescription($other) { return ''; } } string = $this->normalizeText((string)$string); $this->uri = $uri; } protected function matches($other) { $other = $this->normalizeText($other); return mb_stripos($other, $this->string, null, 'UTF-8') !== false; } private function normalizeText($text) { $text = strtr($text, "\r\n", " "); return trim(preg_replace('/\\s{2,}/', ' ', $text)); } public function toString() { return sprintf( 'contains "%s"', $this->string ); } protected function failureDescription($pageContent) { $message = $this->uriMessage('on page'); $message->append("\n--> "); $message->append(substr($pageContent, 0, 300)); if (strlen($pageContent) > 300) { $debugMessage = new Message( "[Content too long to display. See complete response in '" . codecept_output_dir() . "' directory]" ); $message->append("\n")->append($debugMessage); } $message->append("\n--> "); return $message->getMessage() . $this->toString(); } protected function uriMessage($onPage = "") { if (!$this->uri) { return new Message(''); } $message = new Message($this->uri); $message->prepend(" $onPage "); return $message; } } string === '') { return true; } foreach ($nodes as $node) { if (!$node->isDisplayed()) { continue; } if (parent::matches(htmlspecialchars_decode($node->getText()))) { return true; } } return false; } protected function fail($nodes, $selector, ComparisonFailure $comparisonFailure = null) { if (!count($nodes)) { throw new ElementNotFound($selector, 'Element located either by name, CSS or XPath'); } $output = "Failed asserting that any element by " . Locator::humanReadableString($selector); $output .= $this->uriMessage('on page'); if (count($nodes) < 5) { $output .= "\nElements: "; $output .= $this->nodesList($nodes); } else { $message = new Message("[total %s elements]"); $output .= $message->with(count($nodes)); } $output .= "\ncontains text '" . $this->string . "'"; throw new \PHPUnit\Framework\ExpectationFailedException( $output, $comparisonFailure ); } protected function failureDescription($nodes) { $desc = ''; foreach ($nodes as $node) { $desc .= parent::failureDescription($node->getText()); } return $desc; } protected function nodesList($nodes, $contains = null) { $output = ""; foreach ($nodes as $node) { if ($contains && strpos($node->getText(), $contains) === false) { continue; } $message = new Message("\n+ <%s> %s"); $output .= $message->with($node->getTagName(), $node->getText()); } return $output; } } string) { throw new \PHPUnit\Framework\ExpectationFailedException( "Element $selector was found", $comparisonFailure ); } $output = "There was $selector element"; $output .= $this->uriMessage("on page"); $output .= $this->nodesList($nodes, $this->string); $output .= "\ncontaining '{$this->string}'"; throw new \PHPUnit\Framework\ExpectationFailedException( $output, $comparisonFailure ); } public function toString() { if ($this->string) { return 'that contains text "' . $this->string . '"'; } } } getInnerIterator()->current(); if ($test instanceof \PHPUnit\Framework\TestSuite) { return true; } $name = Descriptor::getTestSignature($test); $index = Descriptor::getTestDataSetIndex($test); if (!is_null($index)) { $name .= " with data set #{$index}"; } $accepted = preg_match($this->filter, $name, $matches); if($test instanceof \PHPUnit\Framework\WarningTestCase) { $message = $test->getMessage(); $accepted = preg_match($this->filter, $message, $matches); } if ($accepted && isset($this->filterMax)) { $set = end($matches); $accepted = $set >= $this->filterMin && $set <= $this->filterMax; } return $accepted; } } dispatcher = $dispatcher; } public function addRiskyTest(\PHPUnit\Framework\Test $test, Exception $e, $time) { } public function addFailure(\PHPUnit\Framework\Test $test, \PHPUnit\Framework\AssertionFailedError $e, $time) { $this->unsuccessfulTests[] = spl_object_hash($test); $this->fire(Events::TEST_FAIL, new FailEvent($test, $time, $e)); } public function addError(\PHPUnit\Framework\Test $test, \Exception $e, $time) { $this->unsuccessfulTests[] = spl_object_hash($test); $this->fire(Events::TEST_ERROR, new FailEvent($test, $time, $e)); } public function addWarning(\PHPUnit\Framework\Test $test, \PHPUnit\Framework\Warning $e, $time) { $this->unsuccessfulTests[] = spl_object_hash($test); $this->fire(Events::TEST_WARNING, new FailEvent($test, $time, $e)); } public function addIncompleteTest(\PHPUnit\Framework\Test $test, \Exception $e, $time) { if (in_array(spl_object_hash($test), $this->skippedTests)) { return; } $this->unsuccessfulTests[] = spl_object_hash($test); $this->fire(Events::TEST_INCOMPLETE, new FailEvent($test, $time, $e)); $this->skippedTests[] = spl_object_hash($test); } public function addSkippedTest(\PHPUnit\Framework\Test $test, \Exception $e, $time) { if (in_array(spl_object_hash($test), $this->skippedTests)) { return; } $this->unsuccessfulTests[] = spl_object_hash($test); $this->fire(Events::TEST_SKIPPED, new FailEvent($test, $time, $e)); $this->skippedTests[] = spl_object_hash($test); } public function startTestSuite(\PHPUnit\Framework\TestSuite $suite) { $this->dispatcher->dispatch('suite.start', new SuiteEvent($suite)); } public function endTestSuite(\PHPUnit\Framework\TestSuite $suite) { $this->dispatcher->dispatch('suite.end', new SuiteEvent($suite)); } public function startTest(\PHPUnit\Framework\Test $test) { $this->dispatcher->dispatch(Events::TEST_START, new TestEvent($test)); if (!$test instanceof TestInterface) { return; } if ($test->getMetadata()->isBlocked()) { return; } try { $this->startedTests[] = spl_object_hash($test); $this->fire(Events::TEST_BEFORE, new TestEvent($test)); } catch (\PHPUnit\Framework\IncompleteTestError $e) { $test->getTestResultObject()->addFailure($test, $e, 0); } catch (\PHPUnit\Framework\SkippedTestError $e) { $test->getTestResultObject()->addFailure($test, $e, 0); } catch (\Exception $e) { $test->getTestResultObject()->addError($test, $e, 0); } } public function endTest(\PHPUnit\Framework\Test $test, $time) { $hash = spl_object_hash($test); if (!in_array($hash, $this->unsuccessfulTests)) { $this->fire(Events::TEST_SUCCESS, new TestEvent($test, $time)); } if (in_array($hash, $this->startedTests)) { $this->fire(Events::TEST_AFTER, new TestEvent($test, $time)); } $this->dispatcher->dispatch(Events::TEST_END, new TestEvent($test, $time)); } protected function fire($event, TestEvent $eventType) { $test = $eventType->getTest(); if ($test instanceof TestInterface) { foreach ($test->getMetadata()->getGroups() as $group) { $this->dispatcher->dispatch($event . '.' . $group, $eventType); } } $this->dispatcher->dispatch($event, $eventType); } } currentTestCase = $this->document->createElement('testcase'); $isStrict = Configuration::config()['settings']['strict_xml']; foreach ($test->getReportFields() as $attr => $value) { if ($isStrict and !in_array($attr, $this->strictAttributes)) { continue; } $this->currentTestCase->setAttribute($attr, $value); } } public function endTest(\PHPUnit\Framework\Test $test, $time) { if ($this->currentTestCase !== null and $test instanceof Test) { $numAssertions = $test->getNumAssertions(); $this->testSuiteAssertions[$this->testSuiteLevel] += $numAssertions; $this->currentTestCase->setAttribute( 'assertions', $numAssertions ); } parent::endTest($test, $time); } } getFileName(); } else { $reflector = new \ReflectionClass($test); $filename = $reflector->getFileName(); } if ($filename !== $this->currentFile) { if ($this->currentFile !== null) { parent::endTestSuite(new TestSuite()); } $this->testSuiteAssertions[self::FILE_LEVEL] = 0; $this->testSuiteTests[self::FILE_LEVEL] = 0; $this->testSuiteTimes[self::FILE_LEVEL] = 0; $this->testSuiteErrors[self::FILE_LEVEL] = 0; $this->testSuiteFailures[self::FILE_LEVEL] = 0; $this->testSuiteSkipped[self::FILE_LEVEL] = 0; $this->testSuiteLevel = self::FILE_LEVEL; $this->currentFile = $filename; $this->currentFileSuite = $this->document->createElement('testsuite'); if ($test instanceof Reported) { $reportFields = $test->getReportFields(); $class = isset($reportFields['class']) ? $reportFields['class'] : $reportFields['name']; $this->currentFileSuite->setAttribute('name', $class); } else { $this->currentFileSuite->setAttribute('name', get_class($test)); } $this->currentFileSuite->setAttribute('file', $filename); $this->testSuites[self::SUITE_LEVEL]->appendChild($this->currentFileSuite); $this->testSuites[self::FILE_LEVEL] = $this->currentFileSuite; } if (!$test instanceof Reported) { parent::startTest($test); return; } $this->currentTestCase = $this->document->createElement('testcase'); $isStrict = Configuration::config()['settings']['strict_xml']; foreach ($test->getReportFields() as $attr => $value) { if ($isStrict and !in_array($attr, $this->strictAttributes)) { continue; } $this->currentTestCase->setAttribute($attr, $value); } } public function endTest(\PHPUnit\Framework\Test $test, $time) { if ($this->currentTestCase !== null && $test instanceof Test) { $numAssertions = $test->getNumAssertions(); $this->testSuiteAssertions[$this->testSuiteLevel] += $numAssertions; $this->currentTestCase->setAttribute( 'assertions', $numAssertions ); } if ($test instanceof TestCase) { parent::endTest($test, $time); return; } $this->currentTestCase->setAttribute( 'time', \sprintf('%F', $time) ); $this->testSuites[$this->testSuiteLevel]->appendChild( $this->currentTestCase ); $this->testSuiteTests[$this->testSuiteLevel]++; $this->testSuiteTimes[$this->testSuiteLevel] += $time; $this->currentTestCase = null; } public function endTestSuite(TestSuite $suite) { if ($suite->getName()) { if ($this->currentFile) { parent::endTestSuite(new TestSuite()); $this->currentFile = null; } $this->testSuiteLevel = self::SUITE_LEVEL; } parent::endTestSuite($suite); } } getPrevious() ? $e->getPrevious()->getTrace() : $e->getTrace(); if ($e instanceof \PHPUnit\Framework\ExceptionWrapper) { $trace = $e->getSerializableTrace(); } $eFile = $e->getFile(); $eLine = $e->getLine(); if (!self::frameExists($trace, $eFile, $eLine)) { array_unshift( $trace, ['file' => $eFile, 'line' => $eLine] ); } foreach ($trace as $step) { if (self::classIsFiltered($step) and $filter) { continue; } if (self::fileIsFiltered($step) and $filter) { continue; } if (!$asString) { $stackTrace[] = $step; continue; } if (!isset($step['file'])) { continue; } $stackTrace .= $step['file'] . ':' . $step['line'] . "\n"; } return $stackTrace; } protected static function classIsFiltered($step) { if (!isset($step['class'])) { return false; } $className = $step['class']; foreach (self::$filteredClassesPattern as $filteredClassName) { if (strpos($className, $filteredClassName) === 0) { return true; } } return false; } protected static function fileIsFiltered($step) { if (!isset($step['file'])) { return false; } if (strpos($step['file'], 'codecept.phar/') !== false) { return true; } if (strpos($step['file'], 'vendor' . DIRECTORY_SEPARATOR . 'phpunit') !== false) { return true; } if (strpos($step['file'], 'vendor' . DIRECTORY_SEPARATOR . 'codeception') !== false) { return true; } $modulePath = 'src' . DIRECTORY_SEPARATOR . 'Codeception' . DIRECTORY_SEPARATOR . 'Module'; if (strpos($step['file'], $modulePath) !== false) { return false; } if (strpos($step['file'], 'src' . DIRECTORY_SEPARATOR . 'Codeception' . DIRECTORY_SEPARATOR) !== false) { return true; } return false; } private static function frameExists(array $trace, $file, $line) { foreach ($trace as $frame) { if (isset($frame['file']) && $frame['file'] == $file && isset($frame['line']) && $frame['line'] == $line) { return true; } } return false; } } writeCase( 'error', $time, \PHPUnit\Util\Filter::getFilteredStacktrace($e, false), \PHPUnit\Framework\TestFailure::exceptionToString($e), $test ); $this->currentTestPass = false; } public function addWarning(\PHPUnit\Framework\Test $test, \PHPUnit\Framework\Warning $e, $time) { $this->writeCase( 'warning', $time, \PHPUnit\Util\Filter::getFilteredStacktrace($e, false), \PHPUnit\Framework\TestFailure::exceptionToString($e), $test ); $this->currentTestPass = false; } public function addFailure(\PHPUnit\Framework\Test $test, \PHPUnit\Framework\AssertionFailedError $e, $time) { $this->writeCase( 'fail', $time, \PHPUnit\Util\Filter::getFilteredStacktrace($e, false), \PHPUnit\Framework\TestFailure::exceptionToString($e), $test ); $this->currentTestPass = false; } public function addIncompleteTest(\PHPUnit\Framework\Test $test, \Exception $e, $time) { $this->writeCase( 'error', $time, \PHPUnit\Util\Filter::getFilteredStacktrace($e, false), 'Incomplete Test: ' . $e->getMessage(), $test ); $this->currentTestPass = false; } public function addRiskyTest(\PHPUnit\Framework\Test $test, \Exception $e, $time) { $this->writeCase( 'error', $time, \PHPUnit\Util\Filter::getFilteredStacktrace($e, false), 'Risky Test: ' . $e->getMessage(), $test ); $this->currentTestPass = false; } public function addSkippedTest(\PHPUnit\Framework\Test $test, \Exception $e, $time) { $this->writeCase( 'error', $time, \PHPUnit\Util\Filter::getFilteredStacktrace($e, false), 'Skipped Test: ' . $e->getMessage(), $test ); $this->currentTestPass = false; } public function startTestSuite(\PHPUnit\Framework\TestSuite $suite) { $this->currentTestSuiteName = $suite->getName(); $this->currentTestName = ''; $this->addLogEvent( [ 'event' => 'suiteStart', 'suite' => $this->currentTestSuiteName, 'tests' => count($suite) ] ); } public function endTestSuite(\PHPUnit\Framework\TestSuite $suite) { $this->currentTestSuiteName = ''; $this->currentTestName = ''; $this->writeAll($this->logEvents); } public function startTest(\PHPUnit\Framework\Test $test) { $this->currentTestName = \PHPUnit\Util\Test::describe($test); $this->currentTestPass = true; $this->addLogEvent( [ 'event' => 'testStart', 'suite' => $this->currentTestSuiteName, 'test' => $this->currentTestName ] ); } public function endTest(\PHPUnit\Framework\Test $test, $time) { if ($this->currentTestPass) { $this->writeCase('pass', $time, [], '', $test); } } protected function writeCase($status, $time, array $trace = [], $message = '', $test = null) { $output = ''; if ($test !== null && method_exists($test, 'hasOutput') && $test->hasOutput()) { $output = $test->getActualOutput(); } $this->addLogEvent( [ 'event' => 'test', 'suite' => $this->currentTestSuiteName, 'test' => $this->currentTestName, 'status' => $status, 'time' => $time, 'trace' => $trace, 'message' => \PHPUnit_Util_String::convertToUtf8($message), 'output' => $output, ] ); } protected function addLogEvent($event_data = []) { if (count($event_data)) { array_push($this->logEvents, $event_data); } } public function writeAll($buffer) { array_walk_recursive( $buffer, function (&$input) { if (is_string($input)) { $input = \PHPUnit_Util_String::convertToUtf8($input); } } ); parent::write(json_encode($buffer, JSON_PRETTY_PRINT)); } } } if (!class_exists('\PHPUnit\Util\Log\TAP')) { class TAP extends \PHPUnit\Util\Printer implements \PHPUnit\Framework\TestListener { protected $testNumber = 0; protected $testSuiteLevel = 0; protected $testSuccessful = true; public function __construct($out = null) { parent::__construct($out); $this->write("TAP version 13\n"); } public function addError(\PHPUnit\Framework\Test $test, \Exception $e, $time) { $this->writeNotOk($test, 'Error'); } public function addWarning(\PHPUnit\Framework\Test $test, \PHPUnit\Framework\Warning $e, $time) { $this->writeNotOk($test, 'Warning'); } public function addFailure(\PHPUnit\Framework\Test $test, \PHPUnit\Framework\AssertionFailedError $e, $time) { $this->writeNotOk($test, 'Failure'); $message = explode( "\n", \PHPUnit\Framework\TestFailure::exceptionToString($e) ); $diagnostic = [ 'message' => $message[0], 'severity' => 'fail' ]; if ($e instanceof \PHPUnit\Framework\ExpectationFailedException) { $cf = $e->getComparisonFailure(); if ($cf !== null) { $diagnostic['data'] = [ 'got' => $cf->getActual(), 'expected' => $cf->getExpected() ]; } } $yaml = new \Symfony\Component\Yaml\Dumper; $this->write( sprintf( " ---\n%s ...\n", $yaml->dump($diagnostic, 2, 2) ) ); } public function addIncompleteTest(\PHPUnit\Framework\Test $test, \Exception $e, $time) { $this->writeNotOk($test, '', 'TODO Incomplete Test'); } public function addRiskyTest(\PHPUnit\Framework\Test $test, \Exception $e, $time) { $this->write( sprintf( "ok %d - # RISKY%s\n", $this->testNumber, $e->getMessage() != '' ? ' ' . $e->getMessage() : '' ) ); $this->testSuccessful = false; } public function addSkippedTest(\PHPUnit\Framework\Test $test, \Exception $e, $time) { $this->write( sprintf( "ok %d - # SKIP%s\n", $this->testNumber, $e->getMessage() != '' ? ' ' . $e->getMessage() : '' ) ); $this->testSuccessful = false; } public function startTestSuite(\PHPUnit\Framework\TestSuite $suite) { $this->testSuiteLevel++; } public function endTestSuite(\PHPUnit\Framework\TestSuite $suite) { $this->testSuiteLevel--; if ($this->testSuiteLevel == 0) { $this->write(sprintf("1..%d\n", $this->testNumber)); } } public function startTest(\PHPUnit\Framework\Test $test) { $this->testNumber++; $this->testSuccessful = true; } public function endTest(\PHPUnit\Framework\Test $test, $time) { if ($this->testSuccessful === true) { $this->write( sprintf( "ok %d - %s\n", $this->testNumber, \PHPUnit\Util\Test::describe($test) ) ); } $this->writeDiagnostics($test); } protected function writeNotOk(\PHPUnit\Framework\Test $test, $prefix = '', $directive = '') { $this->write( sprintf( "not ok %d - %s%s%s\n", $this->testNumber, $prefix != '' ? $prefix . ': ' : '', \PHPUnit\Util\Test::describe($test), $directive != '' ? ' # ' . $directive : '' ) ); $this->testSuccessful = false; } private function writeDiagnostics(\PHPUnit\Framework\Test $test) { if (!$test instanceof \PHPUnit\Framework\TestCase) { return; } if (!$test->hasOutput()) { return; } foreach (explode("\n", trim($test->getActualOutput())) as $line) { $this->write( sprintf( "# %s\n", $line ) ); } } } } } templatePath = sprintf( '%s%stemplate%s', __DIR__, DIRECTORY_SEPARATOR, DIRECTORY_SEPARATOR ); } protected function startClass($name) { } public function endTest(\PHPUnit\Framework\Test $test, $time) { $steps = []; $success = ($this->testStatus == \PHPUnit\Runner\BaseTestRunner::STATUS_PASSED); if ($success) { $this->successful++; } if ($test instanceof ScenarioDriven) { $steps = $test->getScenario()->getSteps(); } $this->timeTaken += $time; switch ($this->testStatus) { case \PHPUnit\Runner\BaseTestRunner::STATUS_FAILURE: $scenarioStatus = 'scenarioFailed'; break; case \PHPUnit\Runner\BaseTestRunner::STATUS_SKIPPED: $scenarioStatus = 'scenarioSkipped'; break; case \PHPUnit\Runner\BaseTestRunner::STATUS_INCOMPLETE: $scenarioStatus = 'scenarioIncomplete'; break; case \PHPUnit\Runner\BaseTestRunner::STATUS_ERROR: $scenarioStatus = 'scenarioFailed'; break; default: $scenarioStatus = 'scenarioSuccess'; } $stepsBuffer = ''; $subStepsBuffer = ''; $subStepsRendered = []; foreach ($steps as $step) { if ($step->getMetaStep()) { $subStepsRendered[$step->getMetaStep()->getAction()][] = $this->renderStep($step); } } foreach ($steps as $step) { if ($step->getMetaStep()) { if (! empty($subStepsRendered[$step->getMetaStep()->getAction()])) { $subStepsBuffer = implode('', $subStepsRendered[$step->getMetaStep()->getAction()]); unset($subStepsRendered[$step->getMetaStep()->getAction()]); $stepsBuffer .= $this->renderSubsteps($step->getMetaStep(), $subStepsBuffer); } } else { $stepsBuffer .= $this->renderStep($step); } } $scenarioTemplate = new \Text_Template( $this->templatePath . 'scenario.html' ); $failures = ''; $name = Descriptor::getTestSignatureUnique($test); if (isset($this->failures[$name])) { $failTemplate = new \Text_Template( $this->templatePath . 'fail.html' ); foreach ($this->failures[$name] as $failure) { $failTemplate->setVar(['fail' => nl2br($failure)]); $failures .= $failTemplate->render() . PHP_EOL; } $this->failures[$name] = []; } $png = ''; $html = ''; if ($test instanceof TestInterface) { $reports = $test->getMetadata()->getReports(); if (isset($reports['png'])) { $localPath = PathResolver::getRelativeDir($reports['png'], codecept_output_dir()); $png = "
failure screenshot
"; } if (isset($reports['html'])) { $localPath = PathResolver::getRelativeDir($reports['html'], codecept_output_dir()); $html = "See HTML snapshot of a failed page"; } } $toggle = $stepsBuffer ? '+' : ''; $testString = htmlspecialchars(ucfirst(Descriptor::getTestAsString($test))); $testString = preg_replace('~^([\s\w\\\]+):\s~', '$1 » ', $testString); $scenarioTemplate->setVar( [ 'id' => ++$this->id, 'name' => $testString, 'scenarioStatus' => $scenarioStatus, 'steps' => $stepsBuffer, 'toggle' => $toggle, 'failure' => $failures, 'png' => $png, 'html' => $html, 'time' => round($time, 2) ] ); $this->scenarios .= $scenarioTemplate->render(); } public function startTestSuite(\PHPUnit\Framework\TestSuite $suite) { $suiteTemplate = new \Text_Template( $this->templatePath . 'suite.html' ); if (!$suite->getName()) { return; } $suiteTemplate->setVar(['suite' => ucfirst($suite->getName())]); $this->scenarios .= $suiteTemplate->render(); } protected function endRun() { $scenarioHeaderTemplate = new \Text_Template( $this->templatePath . 'scenario_header.html' ); $status = !$this->failed ? 'OK' : 'FAILED'; $scenarioHeaderTemplate->setVar( [ 'name' => 'Codeception Results', 'status' => $status, 'time' => round($this->timeTaken, 1) ] ); $header = $scenarioHeaderTemplate->render(); $scenariosTemplate = new \Text_Template( $this->templatePath . 'scenarios.html' ); $scenariosTemplate->setVar( [ 'header' => $header, 'scenarios' => $this->scenarios, 'successfulScenarios' => $this->successful, 'failedScenarios' => $this->failed, 'skippedScenarios' => $this->skipped, 'incompleteScenarios' => $this->incomplete ] ); $this->write($scenariosTemplate->render()); } public function addError(\PHPUnit\Framework\Test $test, \Exception $e, $time) { $this->failures[Descriptor::getTestSignatureUnique($test)][] = $this->cleanMessage($e); parent::addError($test, $e, $time); } public function addFailure(\PHPUnit\Framework\Test $test, \PHPUnit\Framework\AssertionFailedError $e, $time) { $this->failures[Descriptor::getTestSignatureUnique($test)][] = $this->cleanMessage($e); parent::addFailure($test, $e, $time); } public function startTest(\PHPUnit\Framework\Test $test) { $name = Descriptor::getTestSignatureUnique($test); if (isset($this->failures[$name])) { return; } parent::startTest($test); } protected function renderStep(Step $step) { $stepTemplate = new \Text_Template($this->templatePath . 'step.html'); $stepTemplate->setVar(['action' => $step->getHtml(), 'error' => $step->hasFailed() ? 'failedStep' : '']); return $stepTemplate->render(); } protected function renderSubsteps(Meta $metaStep, $substepsBuffer) { $metaTemplate = new \Text_Template($this->templatePath . 'substeps.html'); $metaTemplate->setVar(['metaStep' => $metaStep->getHtml(), 'error' => $metaStep->hasFailed() ? 'failedStep' : '', 'steps' => $substepsBuffer, 'id' => uniqid()]); return $metaTemplate->render(); } private function cleanMessage($exception) { $msg = $exception->getMessage(); $msg = str_replace(['','','',''], ['','','',''], $msg); return htmlentities($msg); } } testStatus == \PHPUnit\Runner\BaseTestRunner::STATUS_PASSED); if ($success) { $this->successful++; } if ($this->testStatus == \PHPUnit\Runner\BaseTestRunner::STATUS_FAILURE) { $status = "\033[41;37mFAIL\033[0m"; } elseif ($this->testStatus == \PHPUnit\Runner\BaseTestRunner::STATUS_SKIPPED) { $status = 'Skipped'; } elseif ($this->testStatus == \PHPUnit\Runner\BaseTestRunner::STATUS_INCOMPLETE) { $status = 'Incomplete'; } elseif ($this->testStatus == \PHPUnit\Runner\BaseTestRunner::STATUS_ERROR) { $status = 'ERROR'; } else { $status = 'Ok'; } if (strlen($name) > 75) { $name = substr($name, 0, 70); } $line = $name . str_repeat('.', 75 - strlen($name)); $line .= $status; $this->write($line . "\n"); } protected function endRun() { $this->write("\nCodeception Results\n"); $this->write(sprintf( "Successful: %s. Failed: %s. Incomplete: %s. Skipped: %s", $this->successful, $this->failed, $this->skipped, $this->incomplete ) . "\n"); } public function printResult(\PHPUnit\Framework\TestResult $result) { } public function write($buffer) { } } {fail}

{toggle} {name} {time}s

{steps} {failure} {png} {html}
Test results
{header} {scenarios}

Summary

Successful scenarios: {successfulScenarios}
Failed scenarios: {failedScenarios}
Skipped scenarios: {skippedScenarios}
Incomplete scenarios: {incompleteScenarios}

{name} {status} ({time}s)

    {action}

+ {metaStep}

{steps}

{suite} Tests

OutputInterface::VERBOSITY_NORMAL, $options['colors'] ? 'always' : 'never'); $this->dispatcher = $dispatcher; } protected function printDefect(\PHPUnit\Framework\TestFailure $defect, $count) { $this->write("\n---------\n"); $this->dispatcher->dispatch( Events::TEST_FAIL_PRINT, new FailEvent($defect->failedTest(), null, $defect->thrownException(), $count) ); } protected function printDefectTrace(\PHPUnit\Framework\TestFailure $defect) { $this->write($defect->getExceptionAsString()); $this->writeNewLine(); $stackTrace = \PHPUnit\Util\Filter::getFilteredStacktrace($defect->thrownException(), false); foreach ($stackTrace as $i => $frame) { if (!isset($frame['file'])) { continue; } $this->write( sprintf( "#%d %s(%s)", $i + 1, $frame['file'], isset($frame['line']) ? $frame['line'] : '?' ) ); $this->writeNewLine(); } } public function startTest(\PHPUnit\Framework\Test $test) { if ($test instanceof Unit) { parent::startTest($test); } } public function endTest(\PHPUnit\Framework\Test $test, $time) { if ($test instanceof \PHPUnit\Framework\TestCase or $test instanceof \Codeception\Test\Test) { $this->numAssertions += $test->getNumAssertions(); } $this->lastTestFailed = false; } public function addError(\PHPUnit\Framework\Test $test, \Exception $e, $time) { $this->lastTestFailed = true; } public function addFailure(\PHPUnit\Framework\Test $test, \PHPUnit\Framework\AssertionFailedError $e, $time) { $this->lastTestFailed = true; } public function addWarning(\PHPUnit\Framework\Test $test, \PHPUnit\Framework\Warning $e, $time) { $this->lastTestFailed = true; } public function addIncompleteTest(\PHPUnit\Framework\Test $test, \Exception $e, $time) { $this->lastTestFailed = true; } public function addSkippedTest(\PHPUnit\Framework\Test $test, \Exception $e, $time) { $this->lastTestFailed = true; } } testStatus = \PHPUnit\Runner\BaseTestRunner::STATUS_ERROR; $this->failed++; } public function addFailure(\PHPUnit\Framework\Test $test, \PHPUnit\Framework\AssertionFailedError $e, $time) { $this->testStatus = \PHPUnit\Runner\BaseTestRunner::STATUS_FAILURE; $this->failed++; } public function addWarning(\PHPUnit\Framework\Test $test, \PHPUnit\Framework\Warning $e, $time) { $this->testStatus = \PHPUnit\Runner\BaseTestRunner::STATUS_WARNING; $this->warned++; } public function addIncompleteTest(\PHPUnit\Framework\Test $test, \Exception $e, $time) { $this->testStatus = \PHPUnit\Runner\BaseTestRunner::STATUS_INCOMPLETE; $this->incomplete++; } public function addRiskyTest(\PHPUnit\Framework\Test $test, \Exception $e, $time) { $this->testStatus = \PHPUnit\Runner\BaseTestRunner::STATUS_RISKY; $this->risky++; } public function addSkippedTest(\PHPUnit\Framework\Test $test, \Exception $e, $time) { $this->testStatus = \PHPUnit\Runner\BaseTestRunner::STATUS_SKIPPED; $this->skipped++; } public function startTest(\PHPUnit\Framework\Test $test) { $this->testStatus = \PHPUnit\Runner\BaseTestRunner::STATUS_PASSED; } } false, 'phpunit-xml' => false, 'html' => false, 'tap' => false, 'json' => false, 'report' => false ]; protected $config = []; protected $logDir = null; public function __construct() { $this->config = Configuration::config(); $this->logDir = Configuration::outputDir(); $this->phpUnitOverriders(); parent::__construct(); } public function phpUnitOverriders() { require_once __DIR__ . DIRECTORY_SEPARATOR . 'Overrides/Filter.php'; } public function getPrinter() { return $this->printer; } public function prepareSuite(\PHPUnit\Framework\Test $suite, array &$arguments) { $this->handleConfiguration($arguments); $filterFactory = new \PHPUnit\Runner\Filter\Factory(); if ($arguments['groups']) { $filterFactory->addFilter( new \ReflectionClass('PHPUnit\Runner\Filter\IncludeGroupFilterIterator'), $arguments['groups'] ); } if ($arguments['excludeGroups']) { $filterFactory->addFilter( new \ReflectionClass('PHPUnit\Runner\Filter\ExcludeGroupFilterIterator'), $arguments['excludeGroups'] ); } if ($arguments['filter']) { $filterFactory->addFilter( new \ReflectionClass('Codeception\PHPUnit\FilterTest'), $arguments['filter'] ); } $suite->injectFilter($filterFactory); } public function doEnhancedRun( \PHPUnit\Framework\Test $suite, \PHPUnit\Framework\TestResult $result, array $arguments = [] ) { unset($GLOBALS['app']); $result->convertErrorsToExceptions(false); if (isset($arguments['report_useless_tests'])) { $result->beStrictAboutTestsThatDoNotTestAnything((bool)$arguments['report_useless_tests']); } if (isset($arguments['disallow_test_output'])) { $result->beStrictAboutOutputDuringTests((bool)$arguments['disallow_test_output']); } if (empty(self::$persistentListeners)) { $this->applyReporters($result, $arguments); } if (class_exists('\Symfony\Bridge\PhpUnit\SymfonyTestsListener')) { $arguments['listeners'] = isset($arguments['listeners']) ? $arguments['listeners'] : []; $listener = new \Symfony\Bridge\PhpUnit\SymfonyTestsListener(); $listener->globalListenerDisabled(); $arguments['listeners'][] = $listener; } $arguments['listeners'][] = $this->printer; foreach ($arguments['listeners'] as $listener) { $result->addListener($listener); } $suite->run($result); unset($suite); foreach ($arguments['listeners'] as $listener) { $result->removeListener($listener); } return $result; } protected function applyReporters(\PHPUnit\Framework\TestResult $result, array $arguments) { foreach ($this->defaultListeners as $listener => $value) { if (!isset($arguments[$listener])) { $arguments[$listener] = $value; } } if ($arguments['report']) { self::$persistentListeners[] = $this->instantiateReporter('report'); } if ($arguments['html']) { codecept_debug('Printing HTML report into ' . $arguments['html']); self::$persistentListeners[] = $this->instantiateReporter( 'html', [$this->absolutePath($arguments['html'])] ); } if ($arguments['xml']) { codecept_debug('Printing JUNIT report into ' . $arguments['xml']); self::$persistentListeners[] = $this->instantiateReporter( 'xml', [$this->absolutePath($arguments['xml']), (bool)$arguments['log_incomplete_skipped']] ); } if ($arguments['phpunit-xml']) { codecept_debug('Printing PHPUNIT report into ' . $arguments['phpunit-xml']); self::$persistentListeners[] = $this->instantiateReporter( 'phpunit-xml', [$this->absolutePath($arguments['phpunit-xml']), (bool)$arguments['log_incomplete_skipped']] ); } if ($arguments['tap']) { codecept_debug('Printing TAP report into ' . $arguments['tap']); self::$persistentListeners[] = $this->instantiateReporter('tap', [$this->absolutePath($arguments['tap'])]); } if ($arguments['json']) { codecept_debug('Printing JSON report into ' . $arguments['json']); self::$persistentListeners[] = $this->instantiateReporter( 'json', [$this->absolutePath($arguments['json'])] ); } foreach (self::$persistentListeners as $listener) { if ($listener instanceof ConsolePrinter) { $this->printer = $listener; continue; } $result->addListener($listener); } } protected function instantiateReporter($name, $args = []) { if (!isset($this->config['reporters'][$name])) { throw new ConfigurationException("Reporter $name not defined"); } return (new \ReflectionClass($this->config['reporters'][$name]))->newInstanceArgs($args); } private function absolutePath($path) { if ((strpos($path, '/') === 0) or (strpos($path, ':') === 1)) { return $path; } return $this->logDir . $path; } } _setUp(); } } protected function tearDown() { if (method_exists($this, '_tearDown')) { $this->_tearDown(); } } public static function setUpBeforeClass() { if (method_exists(get_called_class(), '_setUpBeforeClass')) { static::_setUpBeforeClass(); } } public static function tearDownAfterClass() { if (method_exists(get_called_class(), '_tearDownAfterClass')) { static::_tearDownAfterClass(); } } public static function assertStringContainsString($needle, $haystack, $message = '') { if (!is_string($needle)) { throw new AssertionFailedError('Needle is not string'); } if (!is_string($haystack)) { throw new AssertionFailedError('Haystack is not string'); } \Codeception\PHPUnit\TestCase::assertContains($needle, $haystack, $message); } public static function assertStringNotContainsString($needle, $haystack, $message = '') { if (!is_string($needle)) { throw new AssertionFailedError('Needle is not string'); } if (!is_string($haystack)) { throw new AssertionFailedError('Haystack is not string'); } \Codeception\PHPUnit\TestCase::assertNotContains($needle, $haystack, $message); } public static function assertStringContainsStringIgnoringCase($needle, $haystack, $message = '') { if (!is_string($needle)) { throw new AssertionFailedError('Needle is not string'); } if (!is_string($haystack)) { throw new AssertionFailedError('Haystack is not string'); } \Codeception\PHPUnit\TestCase::assertContains($needle, $haystack, $message, true); } public static function assertStringNotContainsStringIgnoringCase($needle, $haystack, $message = '') { if (!is_string($needle)) { throw new AssertionFailedError('Needle is not string'); } if (!is_string($haystack)) { throw new AssertionFailedError('Haystack is not string'); } \Codeception\PHPUnit\TestCase::assertNotContains($needle, $haystack, $message, true); } public static function assertIsArray($actual, $message = '') { \Codeception\PHPUnit\TestCase::assertInternalType('array', $actual, $message); } public static function assertIsBool($actual, $message = '') { \Codeception\PHPUnit\TestCase::assertInternalType('bool', $actual, $message); } public static function assertIsFloat($actual, $message = '') { \Codeception\PHPUnit\TestCase::assertInternalType('float', $actual, $message); } public static function assertIsInt($actual, $message = '') { \Codeception\PHPUnit\TestCase::assertInternalType('int', $actual, $message); } public static function assertIsNumeric($actual, $message = '') { \Codeception\PHPUnit\TestCase::assertInternalType('numeric', $actual, $message); } public static function assertIsObject($actual, $message = '') { \Codeception\PHPUnit\TestCase::assertInternalType('object', $actual, $message); } public static function assertIsResource($actual, $message = '') { \Codeception\PHPUnit\TestCase::assertInternalType('resource', $actual, $message); } public static function assertIsString($actual, $message = '') { \Codeception\PHPUnit\TestCase::assertInternalType('string', $actual, $message); } public static function assertIsScalar($actual, $message = '') { \Codeception\PHPUnit\TestCase::assertInternalType('scalar', $actual, $message); } public static function assertIsCallable($actual, $message = '') { \Codeception\PHPUnit\TestCase::assertInternalType('callable', $actual, $message); } public static function assertIsIterable($actual, $message = '') { \Codeception\PHPUnit\TestCase::assertInternalType('iterable', $actual, $message); } public static function assertIsNotArray($actual, $message = '') { \Codeception\PHPUnit\TestCase::assertNotInternalType('array', $actual, $message); } public static function assertIsNotBool($actual, $message = '') { \Codeception\PHPUnit\TestCase::assertNotInternalType('bool', $actual, $message); } public static function assertIsNotFloat($actual, $message = '') { \Codeception\PHPUnit\TestCase::assertNotInternalType('float', $actual, $message); } public static function assertIsNotInt($actual, $message = '') { \Codeception\PHPUnit\TestCase::assertNotInternalType('int', $actual, $message); } public static function assertIsNotNumeric($actual, $message = '') { \Codeception\PHPUnit\TestCase::assertNotInternalType('numeric', $actual, $message); } public static function assertIsNotObject($actual, $message = '') { \Codeception\PHPUnit\TestCase::assertNotInternalType('object', $actual, $message); } public static function assertIsNotResource($actual, $message = '') { \Codeception\PHPUnit\TestCase::assertNotInternalType('resource', $actual, $message); } public static function assertIsNotString($actual, $message = '') { \Codeception\PHPUnit\TestCase::assertNotInternalType('string', $actual, $message); } public static function assertIsNotScalar($actual, $message = '') { \Codeception\PHPUnit\TestCase::assertNotInternalType('scalar', $actual, $message); } public static function assertIsNotCallable($actual, $message = '') { \Codeception\PHPUnit\TestCase::assertNotInternalType('callable', $actual, $message); } public static function assertIsNotIterable($actual, $message = '') { \Codeception\PHPUnit\TestCase::assertNotInternalType('iterable', $actual, $message); } } test(); $version = file_get_contents('VERSION'); $this->docs(); $this->taskGitHubRelease($version) ->uri('Codeception/Specify') ->askDescription() ->run(); } public function changed($description) { $this->taskChangelog() ->version(file_get_contents('VERSION')) ->change($description) ->run(); } protected $docs = [ 'docs/GlobalConfig.md' => '\Codeception\Specify\Config', 'docs/LocalConfig.md' => '\Codeception\Specify\ConfigBuilder', ]; public function docs() { foreach ($this->docs as $file => $class) { class_exists($class, true); $this->taskGenDoc($file) ->docClass($class) ->processProperty(false) ->run(); } } public function test() { $res = $this->taskPHPUnit()->run(); if (!$res) exit; } } only) { return !in_array($property, $this->only); } return in_array($property, $this->ignore); } public function classIgnored($value) { if (!is_object($value)) return false; return in_array(get_class($value), $this->ignore_classes); } public function propertyIsShallowCloned($property) { if ($this->only and !$this->is_deep) { return in_array($property, $this->only); } if (!$this->is_deep and !in_array($property, $this->deep)) { return true; } return in_array($property, $this->shallow); } public function propertyIsDeeplyCloned($property) { if ($this->only and $this->is_deep) { return in_array($property, $this->only); } if ($this->is_deep and !in_array($property, $this->shallow)) { return true; } return in_array($property, $this->deep); } public static function setDeepClone($deepClone) { self::$deepClone = $deepClone; } public static function setIgnoredClasses($ignoredClasses) { self::$ignoredClasses = $ignoredClasses; } public static function setIgnoredProperties($ignoredProperties) { self::$ignoredProperties = $ignoredProperties; } public static function addIgnoredClasses($ignoredClasses) { self::$ignoredClasses = array_merge(self::$ignoredClasses, $ignoredClasses); } private function __construct() { } static function create() { $config = new Config(); $config->is_deep = self::$deepClone; $config->ignore = self::$ignoredProperties; $config->ignore_classes = self::$ignoredClasses; $config->shallow = array(); $config->deep = array(); $config->only = null; return $config; } }config = $config; if (!$config) { $this->config = Config::create(); } } public function ignore($properties = array()) { if (!is_array($properties)) { $properties = func_get_args(); } $this->config->ignore = array_merge($this->config->ignore, $properties); return $this; } public function ignoreClasses($classes = array()) { $this->config->ignore_classes = array_merge($this->config->ignore_classes, $classes); return $this; } public function deepClone($properties = true) { if (is_bool($properties)) { $this->config->is_deep = $properties; return $this; } if (!is_array($properties)) { $properties = func_get_args(); } $this->config->deep = $properties; return $this; } public function shallowClone($properties = true) { if (is_bool($properties)) { $this->config->is_deep = !$properties; return $this; } if (!is_array($properties)) { $properties = func_get_args(); } $this->config->shallow = array_merge($this->config->shallow, $properties); return $this; } public function cloneOnly($properties) { if (!is_array($properties)) { $properties = func_get_args(); } $this->config->only = $properties; return $this; } } _owner = $owner; $this->_property = $property; if (!($this->_property instanceof \ReflectionProperty)) { $this->_property = new \ReflectionProperty($owner, $this->_property); } $this->_property->setAccessible(true); $this->_initValue = ($value === null ? $this->getValue() : $value); } public function getName() { return $this->_property->getName(); } public function restoreValue() { $this->setValue($this->_initValue); } public function getValue() { return $this->_property->getValue($this->_owner); } public function setValue($value) { $this->_property->setValue($this->_owner, $value); } } copier) return; $this->copier = new \DeepCopy\DeepCopy(); $this->copier->skipUncloneable(); if (!$this->specifyConfig) $this->specifyConfig = Config::create(); } function specify($specification, \Closure $callable = null, $params = []) { if (!$callable) return; $this->specifyInit(); $test = $callable->bindTo($this); $oldName = $this->getName(); $newName = $oldName . ' | ' . $specification; $this->setName($newName); $properties = $this->getSpecifyObjectProperties(); $throws = $this->getSpecifyExpectedException($params); $examples = $this->getSpecifyExamples($params); $showExamplesIndex = $examples !== [[]]; foreach ($examples as $idx => $example) { if ($showExamplesIndex) { $this->setName($newName . ' | examples index ' . $idx); } $this->specifyCloneProperties($properties); if (!empty($this->beforeSpecify) && is_array($this->beforeSpecify)) { foreach ($this->beforeSpecify as $closure) { if ($closure instanceof \Closure) $closure->__invoke(); } } $this->specifyExecute($test, $throws, $example); $this->specifyRestoreProperties($properties); if (!empty($this->afterSpecify) && is_array($this->afterSpecify)) { foreach ($this->afterSpecify as $closure) { if ($closure instanceof \Closure) $closure->__invoke(); } } } $this->setName($oldName); } private function getSpecifyExamples($params) { if (isset($params['examples'])) { if (!is_array($params['examples'])) throw new \RuntimeException("Examples should be an array"); return $params['examples']; } return [[]]; } private function getSpecifyExpectedException($params) { if (isset($params['throws'])) { $throws = (is_array($params['throws'])) ? $params['throws'][0] : $params['throws']; if (is_object($throws)) { $throws = get_class($throws); } if ($throws === 'fail') { $throws = 'PHPUnit_Framework_AssertionFailedError'; } $message = (is_array($params['throws']) && isset($params['throws'][1])) ? $params['throws'][1] : false; return [$throws, $message]; } return false; } private function specifyExecute($test, $throws = false, $examples = array()) { $message = false; if (is_array($throws)) { $message = ($throws[1]) ? strtolower($throws[1]) : false; $throws = $throws[0]; } $result = $this->getTestResultObject(); try { call_user_func_array($test, $examples); $this->specifyCheckMockObjects(); } catch (\PHPUnit_Framework_AssertionFailedError $e) { if ($throws !== get_class($e)){ $result->addFailure(clone($this), $e, $result->time()); } if ($message !==false && $message !== strtolower($e->getMessage())) { $f = new \PHPUnit_Framework_AssertionFailedError("exception message '$message' was expected, but '" . $e->getMessage() . "' was received"); $result->addFailure(clone($this), $f, $result->time()); } } catch (\Exception $e) { if ($throws) { if ($throws !== get_class($e)) { $f = new \PHPUnit_Framework_AssertionFailedError("exception '$throws' was expected, but " . get_class($e) . ' was thrown'); $result->addFailure(clone($this), $f, $result->time()); } if ($message !==false && $message !== strtolower($e->getMessage())) { $f = new \PHPUnit_Framework_AssertionFailedError("exception message '$message' was expected, but '" . $e->getMessage() . "' was received"); $result->addFailure(clone($this), $f, $result->time()); } } else { throw $e; } } if ($throws) { if (isset($e)) { $this->assertTrue(true, 'exception handled'); } else { $f = new \PHPUnit_Framework_AssertionFailedError("exception '$throws' was not thrown as expected"); $result->addFailure(clone($this), $f, $result->time()); } } } public function specifyConfig() { if (!$this->specifyConfig) $this->specifyConfig = Config::create(); return new ConfigBuilder($this->specifyConfig); } function beforeSpecify(\Closure $callable = null) { $this->beforeSpecify[] = $callable->bindTo($this); } function afterSpecify(\Closure $callable = null) { $this->afterSpecify[] = $callable->bindTo($this); } function cleanSpecify() { $this->beforeSpecify = $this->afterSpecify = array(); } private function specifyCloneProperties($properties) { foreach ($properties as $property) { $propertyName = $property->getName(); $propertyValue = $property->getValue(); if ($this->specifyConfig->classIgnored($propertyValue)) { continue; } if ($this->specifyConfig->propertyIsShallowCloned($propertyName)) { if (is_object($propertyValue)) { $property->setValue(clone $propertyValue); } else { $property->setValue($propertyValue); } } if ($this->specifyConfig->propertyIsDeeplyCloned($propertyName)) { $property->setValue($this->copier->copy($propertyValue)); } } } private function specifyRestoreProperties($properties) { foreach ($properties as $property) { $property->restoreValue(); } } private function getSpecifyObjectProperties() { $objectReflection = new \ReflectionObject($this); $propertiesToClone = $objectReflection->getProperties(); if (($classProperties = $this->specifyGetClassPrivateProperties()) !== []) { $propertiesToClone = array_merge($propertiesToClone, $classProperties); } $properties = []; foreach ($propertiesToClone as $property) { if ($this->specifyConfig->propertyIgnored($property->getName())) { continue; } $properties[] = new ObjectProperty($this, $property); } if (($phpUnitReflection = $this->specifyGetPhpUnitReflection()) !== null) { $properties[] = $mockObjects = new ObjectProperty( $this, $phpUnitReflection->getProperty('mockObjects') ); $mockObjects->setValue([]); } return $properties; } private function specifyCheckMockObjects() { if (($phpUnitReflection = $this->specifyGetPhpUnitReflection()) !== null) { $verifyMockObjects = $phpUnitReflection->getMethod('verifyMockObjects'); $verifyMockObjects->setAccessible(true); $verifyMockObjects->invoke($this); } } private function specifyGetClassPrivateProperties() { static $properties = []; if (!isset($properties[__CLASS__])) { $reflection = new \ReflectionClass(__CLASS__); $properties[__CLASS__] = (get_class($this) !== __CLASS__) ? $reflection->getProperties(\ReflectionProperty::IS_PRIVATE) : []; } return $properties[__CLASS__]; } private function specifyGetPhpUnitReflection() { if ($this instanceof \PHPUnit_Framework_TestCase) { return new \ReflectionClass('\PHPUnit_Framework_TestCase'); } } } 'Codeception\Stub', 'docs/Expected.md' => 'Codeception\Stub\Expected', 'docs/StubTrait.md' => 'Codeception\Test\Feature\Stub', ]; public function docs() { foreach ($this->docs as $file => $class) { if (!class_exists($class, true) && !trait_exists($class, true)) { throw new Exception('ups'); } $this->say("Here goes, $class"); $this->taskGenDoc($file) ->docClass($class) ->filterMethods(function(\ReflectionMethod $method) { if ($method->isConstructor() or $method->isDestructor()) return false; if (!$method->isPublic()) return false; if (strpos($method->name, '_') === 0) return false; if (strpos($method->name, 'stub') === 0) return false; return true; }) ->processMethodDocBlock( function (\ReflectionMethod $m, $doc) { $doc = str_replace(array('@since'), array(' * available since version'), $doc); $doc = str_replace(array(' @', "\n@"), array(" * ", "\n * "), $doc); return $doc; }) ->processProperty(false) ->run(); } } }consecutiveMap = $consecutiveMap; } public function getMap() { return $this->consecutiveMap; } }methodMatcher = $matcher; $this->methodValue = $value; } public function getMatcher() { return $this->methodMatcher; } public function getValue() { return $this->methodValue; } } isAbstract()) { $arguments = empty($callables) ? [] : array_keys($callables); $mock = self::generateMockForAbstractClass($class, $arguments, '', false, $testCase); } else { $arguments = empty($callables) ? null : array_keys($callables); $mock = self::generateMock($class, $arguments, [], '', false, $testCase); } self::bindParameters($mock, $params); return self::markAsMock($mock, $reflection); } private static function markAsMock($mock, \ReflectionClass $reflection) { if (!$reflection->hasMethod('__set')) { $mock->__mocked = $reflection->getName(); } return $mock; } public static function factory($class, $num = 1, $params = []) { $objects = []; for ($i = 0; $i < $num; $i++) { $objects[] = self::make($class, $params); } return $objects; } public static function makeEmptyExcept($class, $method, $params = [], $testCase = false) { $class = self::getClassname($class); $reflectionClass = new \ReflectionClass($class); $methods = $reflectionClass->getMethods(); $methods = array_filter( $methods, function ($m) { return !in_array($m->name, Stub::$magicMethods); } ); $methods = array_filter( $methods, function ($m) use ($method) { return $method != $m->name; } ); $methods = array_map( function ($m) { return $m->name; }, $methods ); $methods = count($methods) ? $methods : null; $mock = self::generateMock($class, $methods, [], '', false, $testCase); self::bindParameters($mock, $params); return self::markAsMock($mock, $reflectionClass); } public static function makeEmpty($class, $params = [], $testCase = false) { $class = self::getClassname($class); $reflection = new \ReflectionClass($class); $methods = get_class_methods($class); $methods = array_filter( $methods, function ($i) { return !in_array($i, Stub::$magicMethods); } ); $mock = self::generateMock($class, $methods, [], '', false, $testCase); self::bindParameters($mock, $params); return self::markAsMock($mock, $reflection); } public static function copy($obj, $params = []) { $copy = clone($obj); self::bindParameters($copy, $params); return $copy; } public static function construct($class, $constructorParams = [], $params = [], $testCase = false) { $class = self::getClassname($class); $reflection = new \ReflectionClass($class); $callables = self::getMethodsToReplace($reflection, $params); $arguments = empty($callables) ? null : array_keys($callables); $mock = self::generateMock($class, $arguments, $constructorParams, $testCase); self::bindParameters($mock, $params); return self::markAsMock($mock, $reflection); } public static function constructEmpty($class, $constructorParams = [], $params = [], $testCase = false) { $class = self::getClassname($class); $reflection = new \ReflectionClass($class); $methods = get_class_methods($class); $methods = array_filter( $methods, function ($i) { return !in_array($i, Stub::$magicMethods); } ); $mock = self::generateMock($class, $methods, $constructorParams, $testCase); self::bindParameters($mock, $params); return self::markAsMock($mock, $reflection); } public static function constructEmptyExcept( $class, $method, $constructorParams = [], $params = [], $testCase = false ) { $class = self::getClassname($class); $reflectionClass = new \ReflectionClass($class); $methods = $reflectionClass->getMethods(); $methods = array_filter( $methods, function ($m) { return !in_array($m->name, Stub::$magicMethods); } ); $methods = array_filter( $methods, function ($m) use ($method) { return $method != $m->name; } ); $methods = array_map( function ($m) { return $m->name; }, $methods ); $methods = count($methods) ? $methods : null; $mock = self::generateMock($class, $methods, $constructorParams, $testCase); self::bindParameters($mock, $params); return self::markAsMock($mock, $reflectionClass); } private static function generateMock() { return self::doGenerateMock(func_get_args()); } private static function generateMockForAbstractClass() { return self::doGenerateMock(func_get_args(), true); } private static function doGenerateMock($args, $isAbstract = false) { $testCase = self::extractTestCaseFromArgs($args); $methodName = $isAbstract ? 'getMockForAbstractClass' : 'getMock'; $generatorClass = new Generator; if (version_compare(\PHPUnit\Runner\Version::series(), '5.4', '>=') && $testCase instanceof \PHPUnit\Framework\TestCase ) { $mock = call_user_func_array([$generatorClass, $methodName], $args); $testCase->registerMockObject($mock); return $mock; } if ($testCase instanceof \PHPUnit\Framework\TestCase) { $generatorClass = $testCase; } return call_user_func_array([$generatorClass, $methodName], $args); } private static function extractTestCaseFromArgs(&$args) { $argsLength = count($args) - 1; $testCase = $args[$argsLength]; unset($args[$argsLength]); return $testCase; } public static function update($mock, array $params) { if (!$mock instanceof \PHPUnit\Framework\MockObject\MockObject) { throw new \LogicException('You can update only stubbed objects'); } self::bindParameters($mock, $params); return $mock; } protected static function bindParameters($mock, $params) { $reflectionClass = new \ReflectionClass($mock); if ($mock instanceof \PHPUnit\Framework\MockObject\MockObject) { $parentClass = $reflectionClass->getParentClass(); if ($parentClass !== false) { $reflectionClass = $reflectionClass->getParentClass(); } } foreach ($params as $param => $value) { if ($reflectionClass->hasMethod($param)) { if ($value instanceof StubMarshaler) { $marshaler = $value; $mock ->expects($marshaler->getMatcher()) ->method($param) ->will(new ReturnCallback($marshaler->getValue())); } elseif ($value instanceof \Closure) { $mock ->expects(new AnyInvokedCount) ->method($param) ->will(new ReturnCallback($value)); } elseif ($value instanceof ConsecutiveMap) { $consecutiveMap = $value; $mock ->expects(new AnyInvokedCount) ->method($param) ->will(new ConsecutiveCalls($consecutiveMap->getMap())); } else { $mock ->expects(new AnyInvokedCount) ->method($param) ->will(new ReturnStub($value)); } } elseif ($reflectionClass->hasProperty($param)) { $reflectionProperty = $reflectionClass->getProperty($param); $reflectionProperty->setAccessible(true); $reflectionProperty->setValue($mock, $value); continue; } else { if ($reflectionClass->hasMethod('__set')) { try { $mock->{$param} = $value; } catch (\Exception $e) { throw new \LogicException( sprintf( 'Could not add property %1$s, class %2$s implements __set method, ' . 'and no %1$s property exists', $param, $reflectionClass->getName() ), $e->getCode(), $e ); } } else { $mock->{$param} = $value; } continue; } } } protected static function getClassname($object) { if (is_object($object)) { return get_class($object); } if (is_callable($object)) { return call_user_func($object); } return $object; } protected static function getMethodsToReplace(\ReflectionClass $reflection, $params) { $callables = []; foreach ($params as $method => $value) { if ($reflection->hasMethod($method)) { $callables[$method] = $value; } } return $callables; } public static function consecutive() { return new ConsecutiveMap(func_get_args()); } } mocks = []; } protected function stubEnd($status, $time) { if ($this instanceof \PHPUnit\Framework\TestCase) { return; } if ($status !== 'ok') { return; } foreach ($this->mocks as $mockObject) { if ($mockObject->__phpunit_hasMatchers()) { $this->assertTrue(true); } $mockObject->__phpunit_verify(true); } } public function make($class, $params = []) { return $this->mocks[] = \Codeception\Stub::make($class, $params, $this); } public function makeEmpty($class, $params = []) { return $this->mocks[] = \Codeception\Stub::makeEmpty($class, $params, $this); } public function makeEmptyExcept($class, $method, $params = []) { return $this->mocks[] = \Codeception\Stub::makeEmptyExcept($class, $method, $params, $this); } public function construct($class, $constructorParams = [], $params = []) { return $this->mocks[] = \Codeception\Stub::construct($class, $constructorParams, $params, $this); } public function constructEmpty($class, $constructorParams = [], $params = []) { return $this->mocks[] = \Codeception\Stub::constructEmpty($class, $constructorParams, $params, $this); } public function constructEmptyExcept($class, $method, $constructorParams = [], $params = []) { return $this->mocks[] = \Codeception\Stub::constructEmptyExcept($class, $method, $constructorParams, $params, $this); } } $vendorDir . '/phpunit/php-file-iterator/src/Iterator.php', 'File_Iterator_Facade' => $vendorDir . '/phpunit/php-file-iterator/src/Facade.php', 'File_Iterator_Factory' => $vendorDir . '/phpunit/php-file-iterator/src/Factory.php', 'Generic_Sniffs_Arrays_DisallowLongArraySyntaxSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Generic/Sniffs/Arrays/DisallowLongArraySyntaxSniff.php', 'Generic_Sniffs_Arrays_DisallowShortArraySyntaxSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Generic/Sniffs/Arrays/DisallowShortArraySyntaxSniff.php', 'Generic_Sniffs_Classes_DuplicateClassNameSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Generic/Sniffs/Classes/DuplicateClassNameSniff.php', 'Generic_Sniffs_Classes_OpeningBraceSameLineSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Generic/Sniffs/Classes/OpeningBraceSameLineSniff.php', 'Generic_Sniffs_CodeAnalysis_EmptyStatementSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Generic/Sniffs/CodeAnalysis/EmptyStatementSniff.php', 'Generic_Sniffs_CodeAnalysis_ForLoopShouldBeWhileLoopSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Generic/Sniffs/CodeAnalysis/ForLoopShouldBeWhileLoopSniff.php', 'Generic_Sniffs_CodeAnalysis_ForLoopWithTestFunctionCallSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Generic/Sniffs/CodeAnalysis/ForLoopWithTestFunctionCallSniff.php', 'Generic_Sniffs_CodeAnalysis_JumbledIncrementerSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Generic/Sniffs/CodeAnalysis/JumbledIncrementerSniff.php', 'Generic_Sniffs_CodeAnalysis_UnconditionalIfStatementSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Generic/Sniffs/CodeAnalysis/UnconditionalIfStatementSniff.php', 'Generic_Sniffs_CodeAnalysis_UnnecessaryFinalModifierSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Generic/Sniffs/CodeAnalysis/UnnecessaryFinalModifierSniff.php', 'Generic_Sniffs_CodeAnalysis_UnusedFunctionParameterSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Generic/Sniffs/CodeAnalysis/UnusedFunctionParameterSniff.php', 'Generic_Sniffs_CodeAnalysis_UselessOverridingMethodSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Generic/Sniffs/CodeAnalysis/UselessOverridingMethodSniff.php', 'Generic_Sniffs_Commenting_DocCommentSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Generic/Sniffs/Commenting/DocCommentSniff.php', 'Generic_Sniffs_Commenting_FixmeSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Generic/Sniffs/Commenting/FixmeSniff.php', 'Generic_Sniffs_Commenting_TodoSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Generic/Sniffs/Commenting/TodoSniff.php', 'Generic_Sniffs_ControlStructures_InlineControlStructureSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Generic/Sniffs/ControlStructures/InlineControlStructureSniff.php', 'Generic_Sniffs_Debug_CSSLintSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Generic/Sniffs/Debug/CSSLintSniff.php', 'Generic_Sniffs_Debug_ClosureLinterSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Generic/Sniffs/Debug/ClosureLinterSniff.php', 'Generic_Sniffs_Debug_ESLintSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Generic/Sniffs/Debug/ESLintSniff.php', 'Generic_Sniffs_Debug_JSHintSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Generic/Sniffs/Debug/JSHintSniff.php', 'Generic_Sniffs_Files_ByteOrderMarkSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Generic/Sniffs/Files/ByteOrderMarkSniff.php', 'Generic_Sniffs_Files_EndFileNewlineSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Generic/Sniffs/Files/EndFileNewlineSniff.php', 'Generic_Sniffs_Files_EndFileNoNewlineSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Generic/Sniffs/Files/EndFileNoNewlineSniff.php', 'Generic_Sniffs_Files_InlineHTMLSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Generic/Sniffs/Files/InlineHTMLSniff.php', 'Generic_Sniffs_Files_LineEndingsSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Generic/Sniffs/Files/LineEndingsSniff.php', 'Generic_Sniffs_Files_LineLengthSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Generic/Sniffs/Files/LineLengthSniff.php', 'Generic_Sniffs_Files_LowercasedFilenameSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Generic/Sniffs/Files/LowercasedFilenameSniff.php', 'Generic_Sniffs_Files_OneClassPerFileSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Generic/Sniffs/Files/OneClassPerFileSniff.php', 'Generic_Sniffs_Files_OneInterfacePerFileSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Generic/Sniffs/Files/OneInterfacePerFileSniff.php', 'Generic_Sniffs_Files_OneTraitPerFileSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Generic/Sniffs/Files/OneTraitPerFileSniff.php', 'Generic_Sniffs_Formatting_DisallowMultipleStatementsSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Generic/Sniffs/Formatting/DisallowMultipleStatementsSniff.php', 'Generic_Sniffs_Formatting_MultipleStatementAlignmentSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Generic/Sniffs/Formatting/MultipleStatementAlignmentSniff.php', 'Generic_Sniffs_Formatting_NoSpaceAfterCastSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Generic/Sniffs/Formatting/NoSpaceAfterCastSniff.php', 'Generic_Sniffs_Formatting_SpaceAfterCastSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Generic/Sniffs/Formatting/SpaceAfterCastSniff.php', 'Generic_Sniffs_Formatting_SpaceAfterNotSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Generic/Sniffs/Formatting/SpaceAfterNotSniff.php', 'Generic_Sniffs_Functions_CallTimePassByReferenceSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Generic/Sniffs/Functions/CallTimePassByReferenceSniff.php', 'Generic_Sniffs_Functions_FunctionCallArgumentSpacingSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Generic/Sniffs/Functions/FunctionCallArgumentSpacingSniff.php', 'Generic_Sniffs_Functions_OpeningFunctionBraceBsdAllmanSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Generic/Sniffs/Functions/OpeningFunctionBraceBsdAllmanSniff.php', 'Generic_Sniffs_Functions_OpeningFunctionBraceKernighanRitchieSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Generic/Sniffs/Functions/OpeningFunctionBraceKernighanRitchieSniff.php', 'Generic_Sniffs_Metrics_CyclomaticComplexitySniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Generic/Sniffs/Metrics/CyclomaticComplexitySniff.php', 'Generic_Sniffs_Metrics_NestingLevelSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Generic/Sniffs/Metrics/NestingLevelSniff.php', 'Generic_Sniffs_NamingConventions_CamelCapsFunctionNameSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Generic/Sniffs/NamingConventions/CamelCapsFunctionNameSniff.php', 'Generic_Sniffs_NamingConventions_ConstructorNameSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Generic/Sniffs/NamingConventions/ConstructorNameSniff.php', 'Generic_Sniffs_NamingConventions_UpperCaseConstantNameSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Generic/Sniffs/NamingConventions/UpperCaseConstantNameSniff.php', 'Generic_Sniffs_PHP_BacktickOperatorSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Generic/Sniffs/PHP/BacktickOperatorSniff.php', 'Generic_Sniffs_PHP_CharacterBeforePHPOpeningTagSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Generic/Sniffs/PHP/CharacterBeforePHPOpeningTagSniff.php', 'Generic_Sniffs_PHP_ClosingPHPTagSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Generic/Sniffs/PHP/ClosingPHPTagSniff.php', 'Generic_Sniffs_PHP_DeprecatedFunctionsSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Generic/Sniffs/PHP/DeprecatedFunctionsSniff.php', 'Generic_Sniffs_PHP_DisallowAlternativePHPTagsSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Generic/Sniffs/PHP/DisallowAlternativePHPTagsSniff.php', 'Generic_Sniffs_PHP_DisallowShortOpenTagSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Generic/Sniffs/PHP/DisallowShortOpenTagSniff.php', 'Generic_Sniffs_PHP_ForbiddenFunctionsSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Generic/Sniffs/PHP/ForbiddenFunctionsSniff.php', 'Generic_Sniffs_PHP_LowerCaseConstantSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Generic/Sniffs/PHP/LowerCaseConstantSniff.php', 'Generic_Sniffs_PHP_LowerCaseKeywordSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Generic/Sniffs/PHP/LowerCaseKeywordSniff.php', 'Generic_Sniffs_PHP_NoSilencedErrorsSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Generic/Sniffs/PHP/NoSilencedErrorsSniff.php', 'Generic_Sniffs_PHP_SAPIUsageSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Generic/Sniffs/PHP/SAPIUsageSniff.php', 'Generic_Sniffs_PHP_SyntaxSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Generic/Sniffs/PHP/SyntaxSniff.php', 'Generic_Sniffs_PHP_UpperCaseConstantSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Generic/Sniffs/PHP/UpperCaseConstantSniff.php', 'Generic_Sniffs_Strings_UnnecessaryStringConcatSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Generic/Sniffs/Strings/UnnecessaryStringConcatSniff.php', 'Generic_Sniffs_VersionControl_SubversionPropertiesSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Generic/Sniffs/VersionControl/SubversionPropertiesSniff.php', 'Generic_Sniffs_WhiteSpace_DisallowSpaceIndentSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Generic/Sniffs/WhiteSpace/DisallowSpaceIndentSniff.php', 'Generic_Sniffs_WhiteSpace_DisallowTabIndentSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Generic/Sniffs/WhiteSpace/DisallowTabIndentSniff.php', 'Generic_Sniffs_WhiteSpace_ScopeIndentSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Generic/Sniffs/WhiteSpace/ScopeIndentSniff.php', 'MySource_Sniffs_CSS_BrowserSpecificStylesSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/MySource/Sniffs/CSS/BrowserSpecificStylesSniff.php', 'MySource_Sniffs_Channels_DisallowSelfActionsSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/MySource/Sniffs/Channels/DisallowSelfActionsSniff.php', 'MySource_Sniffs_Channels_IncludeOwnSystemSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/MySource/Sniffs/Channels/IncludeOwnSystemSniff.php', 'MySource_Sniffs_Channels_IncludeSystemSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/MySource/Sniffs/Channels/IncludeSystemSniff.php', 'MySource_Sniffs_Channels_UnusedSystemSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/MySource/Sniffs/Channels/UnusedSystemSniff.php', 'MySource_Sniffs_Commenting_FunctionCommentSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/MySource/Sniffs/Commenting/FunctionCommentSniff.php', 'MySource_Sniffs_Debug_DebugCodeSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/MySource/Sniffs/Debug/DebugCodeSniff.php', 'MySource_Sniffs_Debug_FirebugConsoleSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/MySource/Sniffs/Debug/FirebugConsoleSniff.php', 'MySource_Sniffs_Objects_AssignThisSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/MySource/Sniffs/Objects/AssignThisSniff.php', 'MySource_Sniffs_Objects_CreateWidgetTypeCallbackSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/MySource/Sniffs/Objects/CreateWidgetTypeCallbackSniff.php', 'MySource_Sniffs_Objects_DisallowNewWidgetSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/MySource/Sniffs/Objects/DisallowNewWidgetSniff.php', 'MySource_Sniffs_PHP_AjaxNullComparisonSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/MySource/Sniffs/PHP/AjaxNullComparisonSniff.php', 'MySource_Sniffs_PHP_EvalObjectFactorySniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/MySource/Sniffs/PHP/EvalObjectFactorySniff.php', 'MySource_Sniffs_PHP_GetRequestDataSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/MySource/Sniffs/PHP/GetRequestDataSniff.php', 'MySource_Sniffs_PHP_ReturnFunctionValueSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/MySource/Sniffs/PHP/ReturnFunctionValueSniff.php', 'MySource_Sniffs_Strings_JoinStringsSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/MySource/Sniffs/Strings/JoinStringsSniff.php', 'PEAR_Sniffs_Classes_ClassDeclarationSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/PEAR/Sniffs/Classes/ClassDeclarationSniff.php', 'PEAR_Sniffs_Commenting_ClassCommentSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/PEAR/Sniffs/Commenting/ClassCommentSniff.php', 'PEAR_Sniffs_Commenting_FileCommentSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/PEAR/Sniffs/Commenting/FileCommentSniff.php', 'PEAR_Sniffs_Commenting_FunctionCommentSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/PEAR/Sniffs/Commenting/FunctionCommentSniff.php', 'PEAR_Sniffs_Commenting_InlineCommentSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/PEAR/Sniffs/Commenting/InlineCommentSniff.php', 'PEAR_Sniffs_ControlStructures_ControlSignatureSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/PEAR/Sniffs/ControlStructures/ControlSignatureSniff.php', 'PEAR_Sniffs_ControlStructures_MultiLineConditionSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/PEAR/Sniffs/ControlStructures/MultiLineConditionSniff.php', 'PEAR_Sniffs_Files_IncludingFileSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/PEAR/Sniffs/Files/IncludingFileSniff.php', 'PEAR_Sniffs_Formatting_MultiLineAssignmentSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/PEAR/Sniffs/Formatting/MultiLineAssignmentSniff.php', 'PEAR_Sniffs_Functions_FunctionCallSignatureSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/PEAR/Sniffs/Functions/FunctionCallSignatureSniff.php', 'PEAR_Sniffs_Functions_FunctionDeclarationSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/PEAR/Sniffs/Functions/FunctionDeclarationSniff.php', 'PEAR_Sniffs_Functions_ValidDefaultValueSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/PEAR/Sniffs/Functions/ValidDefaultValueSniff.php', 'PEAR_Sniffs_NamingConventions_ValidClassNameSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/PEAR/Sniffs/NamingConventions/ValidClassNameSniff.php', 'PEAR_Sniffs_NamingConventions_ValidFunctionNameSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/PEAR/Sniffs/NamingConventions/ValidFunctionNameSniff.php', 'PEAR_Sniffs_NamingConventions_ValidVariableNameSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/PEAR/Sniffs/NamingConventions/ValidVariableNameSniff.php', 'PEAR_Sniffs_WhiteSpace_ObjectOperatorIndentSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/PEAR/Sniffs/WhiteSpace/ObjectOperatorIndentSniff.php', 'PEAR_Sniffs_WhiteSpace_ScopeClosingBraceSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/PEAR/Sniffs/WhiteSpace/ScopeClosingBraceSniff.php', 'PEAR_Sniffs_WhiteSpace_ScopeIndentSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/PEAR/Sniffs/WhiteSpace/ScopeIndentSniff.php', 'PHPUnit\\Framework\\Assert' => $vendorDir . '/phpunit/phpunit/src/ForwardCompatibility/Assert.php', 'PHPUnit\\Framework\\AssertionFailedError' => $vendorDir . '/phpunit/phpunit/src/ForwardCompatibility/AssertionFailedError.php', 'PHPUnit\\Framework\\BaseTestListener' => $vendorDir . '/phpunit/phpunit/src/ForwardCompatibility/BaseTestListener.php', 'PHPUnit\\Framework\\Test' => $vendorDir . '/phpunit/phpunit/src/ForwardCompatibility/Test.php', 'PHPUnit\\Framework\\TestCase' => $vendorDir . '/phpunit/phpunit/src/ForwardCompatibility/TestCase.php', 'PHPUnit\\Framework\\TestListener' => $vendorDir . '/phpunit/phpunit/src/ForwardCompatibility/TestListener.php', 'PHPUnit\\Framework\\TestSuite' => $vendorDir . '/phpunit/phpunit/src/ForwardCompatibility/TestSuite.php', 'PHPUnit_Exception' => $vendorDir . '/phpunit/phpunit/src/Exception.php', 'PHPUnit_Extensions_GroupTestSuite' => $vendorDir . '/phpunit/phpunit/src/Extensions/GroupTestSuite.php', 'PHPUnit_Extensions_PhptTestCase' => $vendorDir . '/phpunit/phpunit/src/Extensions/PhptTestCase.php', 'PHPUnit_Extensions_PhptTestSuite' => $vendorDir . '/phpunit/phpunit/src/Extensions/PhptTestSuite.php', 'PHPUnit_Extensions_RepeatedTest' => $vendorDir . '/phpunit/phpunit/src/Extensions/RepeatedTest.php', 'PHPUnit_Extensions_TestDecorator' => $vendorDir . '/phpunit/phpunit/src/Extensions/TestDecorator.php', 'PHPUnit_Extensions_TicketListener' => $vendorDir . '/phpunit/phpunit/src/Extensions/TicketListener.php', 'PHPUnit_Framework_Assert' => $vendorDir . '/phpunit/phpunit/src/Framework/Assert.php', 'PHPUnit_Framework_AssertionFailedError' => $vendorDir . '/phpunit/phpunit/src/Framework/AssertionFailedError.php', 'PHPUnit_Framework_BaseTestListener' => $vendorDir . '/phpunit/phpunit/src/Framework/BaseTestListener.php', 'PHPUnit_Framework_CodeCoverageException' => $vendorDir . '/phpunit/phpunit/src/Framework/CodeCoverageException.php', 'PHPUnit_Framework_Constraint' => $vendorDir . '/phpunit/phpunit/src/Framework/Constraint.php', 'PHPUnit_Framework_Constraint_And' => $vendorDir . '/phpunit/phpunit/src/Framework/Constraint/And.php', 'PHPUnit_Framework_Constraint_ArrayHasKey' => $vendorDir . '/phpunit/phpunit/src/Framework/Constraint/ArrayHasKey.php', 'PHPUnit_Framework_Constraint_ArraySubset' => $vendorDir . '/phpunit/phpunit/src/Framework/Constraint/ArraySubset.php', 'PHPUnit_Framework_Constraint_Attribute' => $vendorDir . '/phpunit/phpunit/src/Framework/Constraint/Attribute.php', 'PHPUnit_Framework_Constraint_Callback' => $vendorDir . '/phpunit/phpunit/src/Framework/Constraint/Callback.php', 'PHPUnit_Framework_Constraint_ClassHasAttribute' => $vendorDir . '/phpunit/phpunit/src/Framework/Constraint/ClassHasAttribute.php', 'PHPUnit_Framework_Constraint_ClassHasStaticAttribute' => $vendorDir . '/phpunit/phpunit/src/Framework/Constraint/ClassHasStaticAttribute.php', 'PHPUnit_Framework_Constraint_Composite' => $vendorDir . '/phpunit/phpunit/src/Framework/Constraint/Composite.php', 'PHPUnit_Framework_Constraint_Count' => $vendorDir . '/phpunit/phpunit/src/Framework/Constraint/Count.php', 'PHPUnit_Framework_Constraint_DirectoryExists' => $vendorDir . '/phpunit/phpunit/src/Framework/Constraint/DirectoryExists.php', 'PHPUnit_Framework_Constraint_Exception' => $vendorDir . '/phpunit/phpunit/src/Framework/Constraint/Exception.php', 'PHPUnit_Framework_Constraint_ExceptionCode' => $vendorDir . '/phpunit/phpunit/src/Framework/Constraint/ExceptionCode.php', 'PHPUnit_Framework_Constraint_ExceptionMessage' => $vendorDir . '/phpunit/phpunit/src/Framework/Constraint/ExceptionMessage.php', 'PHPUnit_Framework_Constraint_ExceptionMessageRegExp' => $vendorDir . '/phpunit/phpunit/src/Framework/Constraint/ExceptionMessageRegExp.php', 'PHPUnit_Framework_Constraint_FileExists' => $vendorDir . '/phpunit/phpunit/src/Framework/Constraint/FileExists.php', 'PHPUnit_Framework_Constraint_GreaterThan' => $vendorDir . '/phpunit/phpunit/src/Framework/Constraint/GreaterThan.php', 'PHPUnit_Framework_Constraint_IsAnything' => $vendorDir . '/phpunit/phpunit/src/Framework/Constraint/IsAnything.php', 'PHPUnit_Framework_Constraint_IsEmpty' => $vendorDir . '/phpunit/phpunit/src/Framework/Constraint/IsEmpty.php', 'PHPUnit_Framework_Constraint_IsEqual' => $vendorDir . '/phpunit/phpunit/src/Framework/Constraint/IsEqual.php', 'PHPUnit_Framework_Constraint_IsFalse' => $vendorDir . '/phpunit/phpunit/src/Framework/Constraint/IsFalse.php', 'PHPUnit_Framework_Constraint_IsFinite' => $vendorDir . '/phpunit/phpunit/src/Framework/Constraint/IsFinite.php', 'PHPUnit_Framework_Constraint_IsIdentical' => $vendorDir . '/phpunit/phpunit/src/Framework/Constraint/IsIdentical.php', 'PHPUnit_Framework_Constraint_IsInfinite' => $vendorDir . '/phpunit/phpunit/src/Framework/Constraint/IsInfinite.php', 'PHPUnit_Framework_Constraint_IsInstanceOf' => $vendorDir . '/phpunit/phpunit/src/Framework/Constraint/IsInstanceOf.php', 'PHPUnit_Framework_Constraint_IsJson' => $vendorDir . '/phpunit/phpunit/src/Framework/Constraint/IsJson.php', 'PHPUnit_Framework_Constraint_IsNan' => $vendorDir . '/phpunit/phpunit/src/Framework/Constraint/IsNan.php', 'PHPUnit_Framework_Constraint_IsNull' => $vendorDir . '/phpunit/phpunit/src/Framework/Constraint/IsNull.php', 'PHPUnit_Framework_Constraint_IsReadable' => $vendorDir . '/phpunit/phpunit/src/Framework/Constraint/IsReadable.php', 'PHPUnit_Framework_Constraint_IsTrue' => $vendorDir . '/phpunit/phpunit/src/Framework/Constraint/IsTrue.php', 'PHPUnit_Framework_Constraint_IsType' => $vendorDir . '/phpunit/phpunit/src/Framework/Constraint/IsType.php', 'PHPUnit_Framework_Constraint_IsWritable' => $vendorDir . '/phpunit/phpunit/src/Framework/Constraint/IsWritable.php', 'PHPUnit_Framework_Constraint_JsonMatches' => $vendorDir . '/phpunit/phpunit/src/Framework/Constraint/JsonMatches.php', 'PHPUnit_Framework_Constraint_JsonMatches_ErrorMessageProvider' => $vendorDir . '/phpunit/phpunit/src/Framework/Constraint/JsonMatches/ErrorMessageProvider.php', 'PHPUnit_Framework_Constraint_LessThan' => $vendorDir . '/phpunit/phpunit/src/Framework/Constraint/LessThan.php', 'PHPUnit_Framework_Constraint_Not' => $vendorDir . '/phpunit/phpunit/src/Framework/Constraint/Not.php', 'PHPUnit_Framework_Constraint_ObjectHasAttribute' => $vendorDir . '/phpunit/phpunit/src/Framework/Constraint/ObjectHasAttribute.php', 'PHPUnit_Framework_Constraint_Or' => $vendorDir . '/phpunit/phpunit/src/Framework/Constraint/Or.php', 'PHPUnit_Framework_Constraint_PCREMatch' => $vendorDir . '/phpunit/phpunit/src/Framework/Constraint/PCREMatch.php', 'PHPUnit_Framework_Constraint_SameSize' => $vendorDir . '/phpunit/phpunit/src/Framework/Constraint/SameSize.php', 'PHPUnit_Framework_Constraint_StringContains' => $vendorDir . '/phpunit/phpunit/src/Framework/Constraint/StringContains.php', 'PHPUnit_Framework_Constraint_StringEndsWith' => $vendorDir . '/phpunit/phpunit/src/Framework/Constraint/StringEndsWith.php', 'PHPUnit_Framework_Constraint_StringMatches' => $vendorDir . '/phpunit/phpunit/src/Framework/Constraint/StringMatches.php', 'PHPUnit_Framework_Constraint_StringStartsWith' => $vendorDir . '/phpunit/phpunit/src/Framework/Constraint/StringStartsWith.php', 'PHPUnit_Framework_Constraint_TraversableContains' => $vendorDir . '/phpunit/phpunit/src/Framework/Constraint/TraversableContains.php', 'PHPUnit_Framework_Constraint_TraversableContainsOnly' => $vendorDir . '/phpunit/phpunit/src/Framework/Constraint/TraversableContainsOnly.php', 'PHPUnit_Framework_Constraint_Xor' => $vendorDir . '/phpunit/phpunit/src/Framework/Constraint/Xor.php', 'PHPUnit_Framework_CoveredCodeNotExecutedException' => $vendorDir . '/phpunit/phpunit/src/Framework/CoveredCodeNotExecutedException.php', 'PHPUnit_Framework_Error' => $vendorDir . '/phpunit/phpunit/src/Framework/Error.php', 'PHPUnit_Framework_Error_Deprecated' => $vendorDir . '/phpunit/phpunit/src/Framework/Error/Deprecated.php', 'PHPUnit_Framework_Error_Notice' => $vendorDir . '/phpunit/phpunit/src/Framework/Error/Notice.php', 'PHPUnit_Framework_Error_Warning' => $vendorDir . '/phpunit/phpunit/src/Framework/Error/Warning.php', 'PHPUnit_Framework_Exception' => $vendorDir . '/phpunit/phpunit/src/Framework/Exception.php', 'PHPUnit_Framework_ExceptionWrapper' => $vendorDir . '/phpunit/phpunit/src/Framework/ExceptionWrapper.php', 'PHPUnit_Framework_ExpectationFailedException' => $vendorDir . '/phpunit/phpunit/src/Framework/ExpectationFailedException.php', 'PHPUnit_Framework_IncompleteTest' => $vendorDir . '/phpunit/phpunit/src/Framework/IncompleteTest.php', 'PHPUnit_Framework_IncompleteTestCase' => $vendorDir . '/phpunit/phpunit/src/Framework/IncompleteTestCase.php', 'PHPUnit_Framework_IncompleteTestError' => $vendorDir . '/phpunit/phpunit/src/Framework/IncompleteTestError.php', 'PHPUnit_Framework_InvalidCoversTargetException' => $vendorDir . '/phpunit/phpunit/src/Framework/InvalidCoversTargetException.php', 'PHPUnit_Framework_MissingCoversAnnotationException' => $vendorDir . '/phpunit/phpunit/src/Framework/MissingCoversAnnotationException.php', 'PHPUnit_Framework_MockObject_BadMethodCallException' => $vendorDir . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Exception/BadMethodCallException.php', 'PHPUnit_Framework_MockObject_Builder_Identity' => $vendorDir . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Builder/Identity.php', 'PHPUnit_Framework_MockObject_Builder_InvocationMocker' => $vendorDir . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Builder/InvocationMocker.php', 'PHPUnit_Framework_MockObject_Builder_Match' => $vendorDir . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Builder/Match.php', 'PHPUnit_Framework_MockObject_Builder_MethodNameMatch' => $vendorDir . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Builder/MethodNameMatch.php', 'PHPUnit_Framework_MockObject_Builder_Namespace' => $vendorDir . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Builder/Namespace.php', 'PHPUnit_Framework_MockObject_Builder_ParametersMatch' => $vendorDir . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Builder/ParametersMatch.php', 'PHPUnit_Framework_MockObject_Builder_Stub' => $vendorDir . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Builder/Stub.php', 'PHPUnit_Framework_MockObject_Exception' => $vendorDir . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Exception/Exception.php', 'PHPUnit_Framework_MockObject_Generator' => $vendorDir . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Generator.php', 'PHPUnit_Framework_MockObject_Invocation' => $vendorDir . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Invocation.php', 'PHPUnit_Framework_MockObject_InvocationMocker' => $vendorDir . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/InvocationMocker.php', 'PHPUnit_Framework_MockObject_Invocation_Object' => $vendorDir . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Invocation/Object.php', 'PHPUnit_Framework_MockObject_Invocation_Static' => $vendorDir . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Invocation/Static.php', 'PHPUnit_Framework_MockObject_Invokable' => $vendorDir . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Invokable.php', 'PHPUnit_Framework_MockObject_Matcher' => $vendorDir . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Matcher.php', 'PHPUnit_Framework_MockObject_Matcher_AnyInvokedCount' => $vendorDir . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Matcher/AnyInvokedCount.php', 'PHPUnit_Framework_MockObject_Matcher_AnyParameters' => $vendorDir . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Matcher/AnyParameters.php', 'PHPUnit_Framework_MockObject_Matcher_ConsecutiveParameters' => $vendorDir . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Matcher/ConsecutiveParameters.php', 'PHPUnit_Framework_MockObject_Matcher_Invocation' => $vendorDir . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Matcher/Invocation.php', 'PHPUnit_Framework_MockObject_Matcher_InvokedAtIndex' => $vendorDir . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Matcher/InvokedAtIndex.php', 'PHPUnit_Framework_MockObject_Matcher_InvokedAtLeastCount' => $vendorDir . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Matcher/InvokedAtLeastCount.php', 'PHPUnit_Framework_MockObject_Matcher_InvokedAtLeastOnce' => $vendorDir . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Matcher/InvokedAtLeastOnce.php', 'PHPUnit_Framework_MockObject_Matcher_InvokedAtMostCount' => $vendorDir . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Matcher/InvokedAtMostCount.php', 'PHPUnit_Framework_MockObject_Matcher_InvokedCount' => $vendorDir . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Matcher/InvokedCount.php', 'PHPUnit_Framework_MockObject_Matcher_InvokedRecorder' => $vendorDir . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Matcher/InvokedRecorder.php', 'PHPUnit_Framework_MockObject_Matcher_MethodName' => $vendorDir . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Matcher/MethodName.php', 'PHPUnit_Framework_MockObject_Matcher_Parameters' => $vendorDir . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Matcher/Parameters.php', 'PHPUnit_Framework_MockObject_Matcher_StatelessInvocation' => $vendorDir . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Matcher/StatelessInvocation.php', 'PHPUnit_Framework_MockObject_MockBuilder' => $vendorDir . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/MockBuilder.php', 'PHPUnit_Framework_MockObject_MockObject' => $vendorDir . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/MockObject.php', 'PHPUnit_Framework_MockObject_RuntimeException' => $vendorDir . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Exception/RuntimeException.php', 'PHPUnit_Framework_MockObject_Stub' => $vendorDir . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Stub.php', 'PHPUnit_Framework_MockObject_Stub_ConsecutiveCalls' => $vendorDir . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Stub/ConsecutiveCalls.php', 'PHPUnit_Framework_MockObject_Stub_Exception' => $vendorDir . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Stub/Exception.php', 'PHPUnit_Framework_MockObject_Stub_MatcherCollection' => $vendorDir . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Stub/MatcherCollection.php', 'PHPUnit_Framework_MockObject_Stub_Return' => $vendorDir . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Stub/Return.php', 'PHPUnit_Framework_MockObject_Stub_ReturnArgument' => $vendorDir . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Stub/ReturnArgument.php', 'PHPUnit_Framework_MockObject_Stub_ReturnCallback' => $vendorDir . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Stub/ReturnCallback.php', 'PHPUnit_Framework_MockObject_Stub_ReturnReference' => $vendorDir . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Stub/ReturnReference.php', 'PHPUnit_Framework_MockObject_Stub_ReturnSelf' => $vendorDir . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Stub/ReturnSelf.php', 'PHPUnit_Framework_MockObject_Stub_ReturnValueMap' => $vendorDir . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Stub/ReturnValueMap.php', 'PHPUnit_Framework_MockObject_Verifiable' => $vendorDir . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Verifiable.php', 'PHPUnit_Framework_OutputError' => $vendorDir . '/phpunit/phpunit/src/Framework/OutputError.php', 'PHPUnit_Framework_RiskyTest' => $vendorDir . '/phpunit/phpunit/src/Framework/RiskyTest.php', 'PHPUnit_Framework_RiskyTestError' => $vendorDir . '/phpunit/phpunit/src/Framework/RiskyTestError.php', 'PHPUnit_Framework_SelfDescribing' => $vendorDir . '/phpunit/phpunit/src/Framework/SelfDescribing.php', 'PHPUnit_Framework_SkippedTest' => $vendorDir . '/phpunit/phpunit/src/Framework/SkippedTest.php', 'PHPUnit_Framework_SkippedTestCase' => $vendorDir . '/phpunit/phpunit/src/Framework/SkippedTestCase.php', 'PHPUnit_Framework_SkippedTestError' => $vendorDir . '/phpunit/phpunit/src/Framework/SkippedTestError.php', 'PHPUnit_Framework_SkippedTestSuiteError' => $vendorDir . '/phpunit/phpunit/src/Framework/SkippedTestSuiteError.php', 'PHPUnit_Framework_SyntheticError' => $vendorDir . '/phpunit/phpunit/src/Framework/SyntheticError.php', 'PHPUnit_Framework_Test' => $vendorDir . '/phpunit/phpunit/src/Framework/Test.php', 'PHPUnit_Framework_TestCase' => $vendorDir . '/phpunit/phpunit/src/Framework/TestCase.php', 'PHPUnit_Framework_TestFailure' => $vendorDir . '/phpunit/phpunit/src/Framework/TestFailure.php', 'PHPUnit_Framework_TestListener' => $vendorDir . '/phpunit/phpunit/src/Framework/TestListener.php', 'PHPUnit_Framework_TestResult' => $vendorDir . '/phpunit/phpunit/src/Framework/TestResult.php', 'PHPUnit_Framework_TestSuite' => $vendorDir . '/phpunit/phpunit/src/Framework/TestSuite.php', 'PHPUnit_Framework_TestSuite_DataProvider' => $vendorDir . '/phpunit/phpunit/src/Framework/TestSuite/DataProvider.php', 'PHPUnit_Framework_UnintentionallyCoveredCodeError' => $vendorDir . '/phpunit/phpunit/src/Framework/UnintentionallyCoveredCodeError.php', 'PHPUnit_Framework_Warning' => $vendorDir . '/phpunit/phpunit/src/Framework/Warning.php', 'PHPUnit_Framework_WarningTestCase' => $vendorDir . '/phpunit/phpunit/src/Framework/WarningTestCase.php', 'PHPUnit_Runner_BaseTestRunner' => $vendorDir . '/phpunit/phpunit/src/Runner/BaseTestRunner.php', 'PHPUnit_Runner_Exception' => $vendorDir . '/phpunit/phpunit/src/Runner/Exception.php', 'PHPUnit_Runner_Filter_Factory' => $vendorDir . '/phpunit/phpunit/src/Runner/Filter/Factory.php', 'PHPUnit_Runner_Filter_GroupFilterIterator' => $vendorDir . '/phpunit/phpunit/src/Runner/Filter/Group.php', 'PHPUnit_Runner_Filter_Group_Exclude' => $vendorDir . '/phpunit/phpunit/src/Runner/Filter/Group/Exclude.php', 'PHPUnit_Runner_Filter_Group_Include' => $vendorDir . '/phpunit/phpunit/src/Runner/Filter/Group/Include.php', 'PHPUnit_Runner_Filter_Test' => $vendorDir . '/phpunit/phpunit/src/Runner/Filter/Test.php', 'PHPUnit_Runner_StandardTestSuiteLoader' => $vendorDir . '/phpunit/phpunit/src/Runner/StandardTestSuiteLoader.php', 'PHPUnit_Runner_TestSuiteLoader' => $vendorDir . '/phpunit/phpunit/src/Runner/TestSuiteLoader.php', 'PHPUnit_Runner_Version' => $vendorDir . '/phpunit/phpunit/src/Runner/Version.php', 'PHPUnit_TextUI_Command' => $vendorDir . '/phpunit/phpunit/src/TextUI/Command.php', 'PHPUnit_TextUI_ResultPrinter' => $vendorDir . '/phpunit/phpunit/src/TextUI/ResultPrinter.php', 'PHPUnit_TextUI_TestRunner' => $vendorDir . '/phpunit/phpunit/src/TextUI/TestRunner.php', 'PHPUnit_Util_Blacklist' => $vendorDir . '/phpunit/phpunit/src/Util/Blacklist.php', 'PHPUnit_Util_Configuration' => $vendorDir . '/phpunit/phpunit/src/Util/Configuration.php', 'PHPUnit_Util_ConfigurationGenerator' => $vendorDir . '/phpunit/phpunit/src/Util/ConfigurationGenerator.php', 'PHPUnit_Util_ErrorHandler' => $vendorDir . '/phpunit/phpunit/src/Util/ErrorHandler.php', 'PHPUnit_Util_Fileloader' => $vendorDir . '/phpunit/phpunit/src/Util/Fileloader.php', 'PHPUnit_Util_Filesystem' => $vendorDir . '/phpunit/phpunit/src/Util/Filesystem.php', 'PHPUnit_Util_Filter' => $vendorDir . '/phpunit/phpunit/src/Util/Filter.php', 'PHPUnit_Util_Getopt' => $vendorDir . '/phpunit/phpunit/src/Util/Getopt.php', 'PHPUnit_Util_GlobalState' => $vendorDir . '/phpunit/phpunit/src/Util/GlobalState.php', 'PHPUnit_Util_InvalidArgumentHelper' => $vendorDir . '/phpunit/phpunit/src/Util/InvalidArgumentHelper.php', 'PHPUnit_Util_Log_JSON' => $vendorDir . '/phpunit/phpunit/src/Util/Log/JSON.php', 'PHPUnit_Util_Log_JUnit' => $vendorDir . '/phpunit/phpunit/src/Util/Log/JUnit.php', 'PHPUnit_Util_Log_TAP' => $vendorDir . '/phpunit/phpunit/src/Util/Log/TAP.php', 'PHPUnit_Util_Log_TeamCity' => $vendorDir . '/phpunit/phpunit/src/Util/Log/TeamCity.php', 'PHPUnit_Util_PHP' => $vendorDir . '/phpunit/phpunit/src/Util/PHP.php', 'PHPUnit_Util_PHP_Default' => $vendorDir . '/phpunit/phpunit/src/Util/PHP/Default.php', 'PHPUnit_Util_PHP_Windows' => $vendorDir . '/phpunit/phpunit/src/Util/PHP/Windows.php', 'PHPUnit_Util_Printer' => $vendorDir . '/phpunit/phpunit/src/Util/Printer.php', 'PHPUnit_Util_Regex' => $vendorDir . '/phpunit/phpunit/src/Util/Regex.php', 'PHPUnit_Util_String' => $vendorDir . '/phpunit/phpunit/src/Util/String.php', 'PHPUnit_Util_Test' => $vendorDir . '/phpunit/phpunit/src/Util/Test.php', 'PHPUnit_Util_TestDox_NamePrettifier' => $vendorDir . '/phpunit/phpunit/src/Util/TestDox/NamePrettifier.php', 'PHPUnit_Util_TestDox_ResultPrinter' => $vendorDir . '/phpunit/phpunit/src/Util/TestDox/ResultPrinter.php', 'PHPUnit_Util_TestDox_ResultPrinter_HTML' => $vendorDir . '/phpunit/phpunit/src/Util/TestDox/ResultPrinter/HTML.php', 'PHPUnit_Util_TestDox_ResultPrinter_Text' => $vendorDir . '/phpunit/phpunit/src/Util/TestDox/ResultPrinter/Text.php', 'PHPUnit_Util_TestDox_ResultPrinter_XML' => $vendorDir . '/phpunit/phpunit/src/Util/TestDox/ResultPrinter/XML.php', 'PHPUnit_Util_TestSuiteIterator' => $vendorDir . '/phpunit/phpunit/src/Util/TestSuiteIterator.php', 'PHPUnit_Util_Type' => $vendorDir . '/phpunit/phpunit/src/Util/Type.php', 'PHPUnit_Util_XML' => $vendorDir . '/phpunit/phpunit/src/Util/XML.php', 'PHP_CodeSniffer' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer.php', 'PHP_CodeSniffer_CLI' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/CLI.php', 'PHP_CodeSniffer_DocGenerators_Generator' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/DocGenerators/Generator.php', 'PHP_CodeSniffer_DocGenerators_HTML' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/DocGenerators/HTML.php', 'PHP_CodeSniffer_DocGenerators_Markdown' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/DocGenerators/Markdown.php', 'PHP_CodeSniffer_DocGenerators_Text' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/DocGenerators/Text.php', 'PHP_CodeSniffer_Exception' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Exception.php', 'PHP_CodeSniffer_File' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/File.php', 'PHP_CodeSniffer_Fixer' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Fixer.php', 'PHP_CodeSniffer_Report' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Report.php', 'PHP_CodeSniffer_Reporting' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Reporting.php', 'PHP_CodeSniffer_Reports_Cbf' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Reports/Cbf.php', 'PHP_CodeSniffer_Reports_Checkstyle' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Reports/Checkstyle.php', 'PHP_CodeSniffer_Reports_Csv' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Reports/Csv.php', 'PHP_CodeSniffer_Reports_Diff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Reports/Diff.php', 'PHP_CodeSniffer_Reports_Emacs' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Reports/Emacs.php', 'PHP_CodeSniffer_Reports_Full' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Reports/Full.php', 'PHP_CodeSniffer_Reports_Gitblame' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Reports/Gitblame.php', 'PHP_CodeSniffer_Reports_Hgblame' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Reports/Hgblame.php', 'PHP_CodeSniffer_Reports_Info' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Reports/Info.php', 'PHP_CodeSniffer_Reports_Json' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Reports/Json.php', 'PHP_CodeSniffer_Reports_Junit' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Reports/Junit.php', 'PHP_CodeSniffer_Reports_Notifysend' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Reports/Notifysend.php', 'PHP_CodeSniffer_Reports_Source' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Reports/Source.php', 'PHP_CodeSniffer_Reports_Summary' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Reports/Summary.php', 'PHP_CodeSniffer_Reports_Svnblame' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Reports/Svnblame.php', 'PHP_CodeSniffer_Reports_VersionControl' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Reports/VersionControl.php', 'PHP_CodeSniffer_Reports_Xml' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Reports/Xml.php', 'PHP_CodeSniffer_Sniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Sniff.php', 'PHP_CodeSniffer_Standards_AbstractPatternSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/AbstractPatternSniff.php', 'PHP_CodeSniffer_Standards_AbstractScopeSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/AbstractScopeSniff.php', 'PHP_CodeSniffer_Standards_AbstractVariableSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/AbstractVariableSniff.php', 'PHP_CodeSniffer_Standards_IncorrectPatternException' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/IncorrectPatternException.php', 'PHP_CodeSniffer_Tokenizers_CSS' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Tokenizers/CSS.php', 'PHP_CodeSniffer_Tokenizers_Comment' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Tokenizers/Comment.php', 'PHP_CodeSniffer_Tokenizers_JS' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Tokenizers/JS.php', 'PHP_CodeSniffer_Tokenizers_PHP' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Tokenizers/PHP.php', 'PHP_CodeSniffer_Tokens' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Tokens.php', 'PHP_Timer' => $vendorDir . '/phpunit/php-timer/src/Timer.php', 'PHP_Token' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_TokenWithScope' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_TokenWithScopeAndVisibility' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_ABSTRACT' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_AMPERSAND' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_AND_EQUAL' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_ARRAY' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_ARRAY_CAST' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_AS' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_ASYNC' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_AT' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_AWAIT' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_BACKTICK' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_BAD_CHARACTER' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_BOOLEAN_AND' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_BOOLEAN_OR' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_BOOL_CAST' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_BREAK' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_CALLABLE' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_CARET' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_CASE' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_CATCH' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_CHARACTER' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_CLASS' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_CLASS_C' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_CLASS_NAME_CONSTANT' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_CLONE' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_CLOSE_BRACKET' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_CLOSE_CURLY' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_CLOSE_SQUARE' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_CLOSE_TAG' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_COALESCE' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_COLON' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_COMMA' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_COMMENT' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_COMPILER_HALT_OFFSET' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_CONCAT_EQUAL' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_CONST' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_CONSTANT_ENCAPSED_STRING' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_CONTINUE' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_CURLY_OPEN' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_DEC' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_DECLARE' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_DEFAULT' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_DIR' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_DIV' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_DIV_EQUAL' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_DNUMBER' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_DO' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_DOC_COMMENT' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_DOLLAR' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_DOLLAR_OPEN_CURLY_BRACES' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_DOT' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_DOUBLE_ARROW' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_DOUBLE_CAST' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_DOUBLE_COLON' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_DOUBLE_QUOTES' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_ECHO' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_ELLIPSIS' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_ELSE' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_ELSEIF' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_EMPTY' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_ENCAPSED_AND_WHITESPACE' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_ENDDECLARE' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_ENDFOR' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_ENDFOREACH' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_ENDIF' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_ENDSWITCH' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_ENDWHILE' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_END_HEREDOC' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_ENUM' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_EQUAL' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_EQUALS' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_EVAL' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_EXCLAMATION_MARK' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_EXIT' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_EXTENDS' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_FILE' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_FINAL' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_FINALLY' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_FOR' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_FOREACH' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_FUNCTION' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_FUNC_C' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_GLOBAL' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_GOTO' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_GT' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_HALT_COMPILER' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_IF' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_IMPLEMENTS' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_IN' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_INC' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_INCLUDE' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_INCLUDE_ONCE' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_INLINE_HTML' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_INSTANCEOF' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_INSTEADOF' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_INTERFACE' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_INT_CAST' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_ISSET' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_IS_EQUAL' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_IS_GREATER_OR_EQUAL' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_IS_IDENTICAL' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_IS_NOT_EQUAL' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_IS_NOT_IDENTICAL' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_IS_SMALLER_OR_EQUAL' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_Includes' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_JOIN' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_LAMBDA_ARROW' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_LAMBDA_CP' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_LAMBDA_OP' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_LINE' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_LIST' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_LNUMBER' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_LOGICAL_AND' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_LOGICAL_OR' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_LOGICAL_XOR' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_LT' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_METHOD_C' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_MINUS' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_MINUS_EQUAL' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_MOD_EQUAL' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_MULT' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_MUL_EQUAL' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_NAMESPACE' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_NEW' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_NS_C' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_NS_SEPARATOR' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_NULLSAFE_OBJECT_OPERATOR' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_NUM_STRING' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_OBJECT_CAST' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_OBJECT_OPERATOR' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_ONUMBER' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_OPEN_BRACKET' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_OPEN_CURLY' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_OPEN_SQUARE' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_OPEN_TAG' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_OPEN_TAG_WITH_ECHO' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_OR_EQUAL' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_PAAMAYIM_NEKUDOTAYIM' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_PERCENT' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_PIPE' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_PLUS' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_PLUS_EQUAL' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_POW' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_POW_EQUAL' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_PRINT' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_PRIVATE' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_PROTECTED' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_PUBLIC' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_QUESTION_MARK' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_REQUIRE' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_REQUIRE_ONCE' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_RETURN' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_SEMICOLON' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_SHAPE' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_SL' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_SL_EQUAL' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_SPACESHIP' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_SR' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_SR_EQUAL' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_START_HEREDOC' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_STATIC' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_STRING' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_STRING_CAST' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_STRING_VARNAME' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_SUPER' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_SWITCH' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_Stream' => $vendorDir . '/phpunit/php-token-stream/src/Token/Stream.php', 'PHP_Token_Stream_CachingFactory' => $vendorDir . '/phpunit/php-token-stream/src/Token/Stream/CachingFactory.php', 'PHP_Token_THROW' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_TILDE' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_TRAIT' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_TRAIT_C' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_TRY' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_TYPE' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_TYPELIST_GT' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_TYPELIST_LT' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_UNSET' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_UNSET_CAST' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_USE' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_USE_FUNCTION' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_VAR' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_VARIABLE' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_WHERE' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_WHILE' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_WHITESPACE' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_XHP_ATTRIBUTE' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_XHP_CATEGORY' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_XHP_CATEGORY_LABEL' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_XHP_CHILDREN' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_XHP_LABEL' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_XHP_REQUIRED' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_XHP_TAG_GT' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_XHP_TAG_LT' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_XHP_TEXT' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_XOR_EQUAL' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_YIELD' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_YIELD_FROM' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PSR1_Sniffs_Classes_ClassDeclarationSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/PSR1/Sniffs/Classes/ClassDeclarationSniff.php', 'PSR1_Sniffs_Files_SideEffectsSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/PSR1/Sniffs/Files/SideEffectsSniff.php', 'PSR1_Sniffs_Methods_CamelCapsMethodNameSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/PSR1/Sniffs/Methods/CamelCapsMethodNameSniff.php', 'PSR2_Sniffs_Classes_ClassDeclarationSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/PSR2/Sniffs/Classes/ClassDeclarationSniff.php', 'PSR2_Sniffs_Classes_PropertyDeclarationSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/PSR2/Sniffs/Classes/PropertyDeclarationSniff.php', 'PSR2_Sniffs_ControlStructures_ControlStructureSpacingSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/PSR2/Sniffs/ControlStructures/ControlStructureSpacingSniff.php', 'PSR2_Sniffs_ControlStructures_ElseIfDeclarationSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/PSR2/Sniffs/ControlStructures/ElseIfDeclarationSniff.php', 'PSR2_Sniffs_ControlStructures_SwitchDeclarationSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/PSR2/Sniffs/ControlStructures/SwitchDeclarationSniff.php', 'PSR2_Sniffs_Files_ClosingTagSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/PSR2/Sniffs/Files/ClosingTagSniff.php', 'PSR2_Sniffs_Files_EndFileNewlineSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/PSR2/Sniffs/Files/EndFileNewlineSniff.php', 'PSR2_Sniffs_Methods_FunctionCallSignatureSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/PSR2/Sniffs/Methods/FunctionCallSignatureSniff.php', 'PSR2_Sniffs_Methods_FunctionClosingBraceSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/PSR2/Sniffs/Methods/FunctionClosingBraceSniff.php', 'PSR2_Sniffs_Methods_MethodDeclarationSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/PSR2/Sniffs/Methods/MethodDeclarationSniff.php', 'PSR2_Sniffs_Namespaces_NamespaceDeclarationSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/PSR2/Sniffs/Namespaces/NamespaceDeclarationSniff.php', 'PSR2_Sniffs_Namespaces_UseDeclarationSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/PSR2/Sniffs/Namespaces/UseDeclarationSniff.php', 'SebastianBergmann\\CodeCoverage\\CodeCoverage' => $vendorDir . '/phpunit/php-code-coverage/src/CodeCoverage.php', 'SebastianBergmann\\CodeCoverage\\CoveredCodeNotExecutedException' => $vendorDir . '/phpunit/php-code-coverage/src/Exception/CoveredCodeNotExecutedException.php', 'SebastianBergmann\\CodeCoverage\\Driver\\Driver' => $vendorDir . '/phpunit/php-code-coverage/src/Driver/Driver.php', 'SebastianBergmann\\CodeCoverage\\Driver\\HHVM' => $vendorDir . '/phpunit/php-code-coverage/src/Driver/HHVM.php', 'SebastianBergmann\\CodeCoverage\\Driver\\PHPDBG' => $vendorDir . '/phpunit/php-code-coverage/src/Driver/PHPDBG.php', 'SebastianBergmann\\CodeCoverage\\Driver\\Xdebug' => $vendorDir . '/phpunit/php-code-coverage/src/Driver/Xdebug.php', 'SebastianBergmann\\CodeCoverage\\Exception' => $vendorDir . '/phpunit/php-code-coverage/src/Exception/Exception.php', 'SebastianBergmann\\CodeCoverage\\Filter' => $vendorDir . '/phpunit/php-code-coverage/src/Filter.php', 'SebastianBergmann\\CodeCoverage\\InvalidArgumentException' => $vendorDir . '/phpunit/php-code-coverage/src/Exception/InvalidArgumentException.php', 'SebastianBergmann\\CodeCoverage\\MissingCoversAnnotationException' => $vendorDir . '/phpunit/php-code-coverage/src/Exception/MissingCoversAnnotationException.php', 'SebastianBergmann\\CodeCoverage\\Node\\AbstractNode' => $vendorDir . '/phpunit/php-code-coverage/src/Node/AbstractNode.php', 'SebastianBergmann\\CodeCoverage\\Node\\Builder' => $vendorDir . '/phpunit/php-code-coverage/src/Node/Builder.php', 'SebastianBergmann\\CodeCoverage\\Node\\Directory' => $vendorDir . '/phpunit/php-code-coverage/src/Node/Directory.php', 'SebastianBergmann\\CodeCoverage\\Node\\File' => $vendorDir . '/phpunit/php-code-coverage/src/Node/File.php', 'SebastianBergmann\\CodeCoverage\\Node\\Iterator' => $vendorDir . '/phpunit/php-code-coverage/src/Node/Iterator.php', 'SebastianBergmann\\CodeCoverage\\Report\\Clover' => $vendorDir . '/phpunit/php-code-coverage/src/Report/Clover.php', 'SebastianBergmann\\CodeCoverage\\Report\\Crap4j' => $vendorDir . '/phpunit/php-code-coverage/src/Report/Crap4j.php', 'SebastianBergmann\\CodeCoverage\\Report\\Html\\Dashboard' => $vendorDir . '/phpunit/php-code-coverage/src/Report/Html/Renderer/Dashboard.php', 'SebastianBergmann\\CodeCoverage\\Report\\Html\\Directory' => $vendorDir . '/phpunit/php-code-coverage/src/Report/Html/Renderer/Directory.php', 'SebastianBergmann\\CodeCoverage\\Report\\Html\\Facade' => $vendorDir . '/phpunit/php-code-coverage/src/Report/Html/Facade.php', 'SebastianBergmann\\CodeCoverage\\Report\\Html\\File' => $vendorDir . '/phpunit/php-code-coverage/src/Report/Html/Renderer/File.php', 'SebastianBergmann\\CodeCoverage\\Report\\Html\\Renderer' => $vendorDir . '/phpunit/php-code-coverage/src/Report/Html/Renderer.php', 'SebastianBergmann\\CodeCoverage\\Report\\PHP' => $vendorDir . '/phpunit/php-code-coverage/src/Report/PHP.php', 'SebastianBergmann\\CodeCoverage\\Report\\Text' => $vendorDir . '/phpunit/php-code-coverage/src/Report/Text.php', 'SebastianBergmann\\CodeCoverage\\Report\\Xml\\Coverage' => $vendorDir . '/phpunit/php-code-coverage/src/Report/Xml/Coverage.php', 'SebastianBergmann\\CodeCoverage\\Report\\Xml\\Directory' => $vendorDir . '/phpunit/php-code-coverage/src/Report/Xml/Directory.php', 'SebastianBergmann\\CodeCoverage\\Report\\Xml\\Facade' => $vendorDir . '/phpunit/php-code-coverage/src/Report/Xml/Facade.php', 'SebastianBergmann\\CodeCoverage\\Report\\Xml\\File' => $vendorDir . '/phpunit/php-code-coverage/src/Report/Xml/File.php', 'SebastianBergmann\\CodeCoverage\\Report\\Xml\\Method' => $vendorDir . '/phpunit/php-code-coverage/src/Report/Xml/Method.php', 'SebastianBergmann\\CodeCoverage\\Report\\Xml\\Node' => $vendorDir . '/phpunit/php-code-coverage/src/Report/Xml/Node.php', 'SebastianBergmann\\CodeCoverage\\Report\\Xml\\Project' => $vendorDir . '/phpunit/php-code-coverage/src/Report/Xml/Project.php', 'SebastianBergmann\\CodeCoverage\\Report\\Xml\\Report' => $vendorDir . '/phpunit/php-code-coverage/src/Report/Xml/Report.php', 'SebastianBergmann\\CodeCoverage\\Report\\Xml\\Tests' => $vendorDir . '/phpunit/php-code-coverage/src/Report/Xml/Tests.php', 'SebastianBergmann\\CodeCoverage\\Report\\Xml\\Totals' => $vendorDir . '/phpunit/php-code-coverage/src/Report/Xml/Totals.php', 'SebastianBergmann\\CodeCoverage\\Report\\Xml\\Unit' => $vendorDir . '/phpunit/php-code-coverage/src/Report/Xml/Unit.php', 'SebastianBergmann\\CodeCoverage\\RuntimeException' => $vendorDir . '/phpunit/php-code-coverage/src/Exception/RuntimeException.php', 'SebastianBergmann\\CodeCoverage\\UnintentionallyCoveredCodeException' => $vendorDir . '/phpunit/php-code-coverage/src/Exception/UnintentionallyCoveredCodeException.php', 'SebastianBergmann\\CodeCoverage\\Util' => $vendorDir . '/phpunit/php-code-coverage/src/Util.php', 'SebastianBergmann\\CodeUnitReverseLookup\\Wizard' => $vendorDir . '/sebastian/code-unit-reverse-lookup/src/Wizard.php', 'SebastianBergmann\\Comparator\\ArrayComparator' => $vendorDir . '/sebastian/comparator/src/ArrayComparator.php', 'SebastianBergmann\\Comparator\\Comparator' => $vendorDir . '/sebastian/comparator/src/Comparator.php', 'SebastianBergmann\\Comparator\\ComparisonFailure' => $vendorDir . '/sebastian/comparator/src/ComparisonFailure.php', 'SebastianBergmann\\Comparator\\DOMNodeComparator' => $vendorDir . '/sebastian/comparator/src/DOMNodeComparator.php', 'SebastianBergmann\\Comparator\\DateTimeComparator' => $vendorDir . '/sebastian/comparator/src/DateTimeComparator.php', 'SebastianBergmann\\Comparator\\DoubleComparator' => $vendorDir . '/sebastian/comparator/src/DoubleComparator.php', 'SebastianBergmann\\Comparator\\ExceptionComparator' => $vendorDir . '/sebastian/comparator/src/ExceptionComparator.php', 'SebastianBergmann\\Comparator\\Factory' => $vendorDir . '/sebastian/comparator/src/Factory.php', 'SebastianBergmann\\Comparator\\MockObjectComparator' => $vendorDir . '/sebastian/comparator/src/MockObjectComparator.php', 'SebastianBergmann\\Comparator\\NumericComparator' => $vendorDir . '/sebastian/comparator/src/NumericComparator.php', 'SebastianBergmann\\Comparator\\ObjectComparator' => $vendorDir . '/sebastian/comparator/src/ObjectComparator.php', 'SebastianBergmann\\Comparator\\ResourceComparator' => $vendorDir . '/sebastian/comparator/src/ResourceComparator.php', 'SebastianBergmann\\Comparator\\ScalarComparator' => $vendorDir . '/sebastian/comparator/src/ScalarComparator.php', 'SebastianBergmann\\Comparator\\SplObjectStorageComparator' => $vendorDir . '/sebastian/comparator/src/SplObjectStorageComparator.php', 'SebastianBergmann\\Comparator\\TypeComparator' => $vendorDir . '/sebastian/comparator/src/TypeComparator.php', 'SebastianBergmann\\Diff\\Chunk' => $vendorDir . '/sebastian/diff/src/Chunk.php', 'SebastianBergmann\\Diff\\Diff' => $vendorDir . '/sebastian/diff/src/Diff.php', 'SebastianBergmann\\Diff\\Differ' => $vendorDir . '/sebastian/diff/src/Differ.php', 'SebastianBergmann\\Diff\\LCS\\LongestCommonSubsequence' => $vendorDir . '/sebastian/diff/src/LCS/LongestCommonSubsequence.php', 'SebastianBergmann\\Diff\\LCS\\MemoryEfficientImplementation' => $vendorDir . '/sebastian/diff/src/LCS/MemoryEfficientLongestCommonSubsequenceImplementation.php', 'SebastianBergmann\\Diff\\LCS\\TimeEfficientImplementation' => $vendorDir . '/sebastian/diff/src/LCS/TimeEfficientLongestCommonSubsequenceImplementation.php', 'SebastianBergmann\\Diff\\Line' => $vendorDir . '/sebastian/diff/src/Line.php', 'SebastianBergmann\\Diff\\Parser' => $vendorDir . '/sebastian/diff/src/Parser.php', 'SebastianBergmann\\Environment\\Console' => $vendorDir . '/sebastian/environment/src/Console.php', 'SebastianBergmann\\Environment\\Runtime' => $vendorDir . '/sebastian/environment/src/Runtime.php', 'SebastianBergmann\\Exporter\\Exporter' => $vendorDir . '/sebastian/exporter/src/Exporter.php', 'SebastianBergmann\\GlobalState\\Blacklist' => $vendorDir . '/sebastian/global-state/src/Blacklist.php', 'SebastianBergmann\\GlobalState\\CodeExporter' => $vendorDir . '/sebastian/global-state/src/CodeExporter.php', 'SebastianBergmann\\GlobalState\\Exception' => $vendorDir . '/sebastian/global-state/src/Exception.php', 'SebastianBergmann\\GlobalState\\Restorer' => $vendorDir . '/sebastian/global-state/src/Restorer.php', 'SebastianBergmann\\GlobalState\\RuntimeException' => $vendorDir . '/sebastian/global-state/src/RuntimeException.php', 'SebastianBergmann\\GlobalState\\Snapshot' => $vendorDir . '/sebastian/global-state/src/Snapshot.php', 'SebastianBergmann\\ObjectEnumerator\\Enumerator' => $vendorDir . '/sebastian/object-enumerator/src/Enumerator.php', 'SebastianBergmann\\ObjectEnumerator\\Exception' => $vendorDir . '/sebastian/object-enumerator/src/Exception.php', 'SebastianBergmann\\ObjectEnumerator\\InvalidArgumentException' => $vendorDir . '/sebastian/object-enumerator/src/InvalidArgumentException.php', 'SebastianBergmann\\RecursionContext\\Context' => $vendorDir . '/sebastian/recursion-context/src/Context.php', 'SebastianBergmann\\RecursionContext\\Exception' => $vendorDir . '/sebastian/recursion-context/src/Exception.php', 'SebastianBergmann\\RecursionContext\\InvalidArgumentException' => $vendorDir . '/sebastian/recursion-context/src/InvalidArgumentException.php', 'SebastianBergmann\\ResourceOperations\\ResourceOperations' => $vendorDir . '/sebastian/resource-operations/src/ResourceOperations.php', 'SebastianBergmann\\Version' => $vendorDir . '/sebastian/version/src/Version.php', 'Squiz_Sniffs_Arrays_ArrayBracketSpacingSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/Arrays/ArrayBracketSpacingSniff.php', 'Squiz_Sniffs_Arrays_ArrayDeclarationSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/Arrays/ArrayDeclarationSniff.php', 'Squiz_Sniffs_CSS_ClassDefinitionClosingBraceSpaceSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/CSS/ClassDefinitionClosingBraceSpaceSniff.php', 'Squiz_Sniffs_CSS_ClassDefinitionNameSpacingSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/CSS/ClassDefinitionNameSpacingSniff.php', 'Squiz_Sniffs_CSS_ClassDefinitionOpeningBraceSpaceSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/CSS/ClassDefinitionOpeningBraceSpaceSniff.php', 'Squiz_Sniffs_CSS_ColonSpacingSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/CSS/ColonSpacingSniff.php', 'Squiz_Sniffs_CSS_ColourDefinitionSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/CSS/ColourDefinitionSniff.php', 'Squiz_Sniffs_CSS_DisallowMultipleStyleDefinitionsSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/CSS/DisallowMultipleStyleDefinitionsSniff.php', 'Squiz_Sniffs_CSS_DuplicateClassDefinitionSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/CSS/DuplicateClassDefinitionSniff.php', 'Squiz_Sniffs_CSS_DuplicateStyleDefinitionSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/CSS/DuplicateStyleDefinitionSniff.php', 'Squiz_Sniffs_CSS_EmptyClassDefinitionSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/CSS/EmptyClassDefinitionSniff.php', 'Squiz_Sniffs_CSS_EmptyStyleDefinitionSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/CSS/EmptyStyleDefinitionSniff.php', 'Squiz_Sniffs_CSS_ForbiddenStylesSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/CSS/ForbiddenStylesSniff.php', 'Squiz_Sniffs_CSS_IndentationSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/CSS/IndentationSniff.php', 'Squiz_Sniffs_CSS_LowercaseStyleDefinitionSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/CSS/LowercaseStyleDefinitionSniff.php', 'Squiz_Sniffs_CSS_MissingColonSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/CSS/MissingColonSniff.php', 'Squiz_Sniffs_CSS_NamedColoursSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/CSS/NamedColoursSniff.php', 'Squiz_Sniffs_CSS_OpacitySniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/CSS/OpacitySniff.php', 'Squiz_Sniffs_CSS_SemicolonSpacingSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/CSS/SemicolonSpacingSniff.php', 'Squiz_Sniffs_CSS_ShorthandSizeSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/CSS/ShorthandSizeSniff.php', 'Squiz_Sniffs_Classes_ClassDeclarationSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/Classes/ClassDeclarationSniff.php', 'Squiz_Sniffs_Classes_ClassFileNameSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/Classes/ClassFileNameSniff.php', 'Squiz_Sniffs_Classes_DuplicatePropertySniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/Classes/DuplicatePropertySniff.php', 'Squiz_Sniffs_Classes_LowercaseClassKeywordsSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/Classes/LowercaseClassKeywordsSniff.php', 'Squiz_Sniffs_Classes_SelfMemberReferenceSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/Classes/SelfMemberReferenceSniff.php', 'Squiz_Sniffs_Classes_ValidClassNameSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/Classes/ValidClassNameSniff.php', 'Squiz_Sniffs_Commenting_BlockCommentSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/Commenting/BlockCommentSniff.php', 'Squiz_Sniffs_Commenting_ClassCommentSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/Commenting/ClassCommentSniff.php', 'Squiz_Sniffs_Commenting_ClosingDeclarationCommentSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/Commenting/ClosingDeclarationCommentSniff.php', 'Squiz_Sniffs_Commenting_DocCommentAlignmentSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/Commenting/DocCommentAlignmentSniff.php', 'Squiz_Sniffs_Commenting_EmptyCatchCommentSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/Commenting/EmptyCatchCommentSniff.php', 'Squiz_Sniffs_Commenting_FileCommentSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/Commenting/FileCommentSniff.php', 'Squiz_Sniffs_Commenting_FunctionCommentSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/Commenting/FunctionCommentSniff.php', 'Squiz_Sniffs_Commenting_FunctionCommentThrowTagSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/Commenting/FunctionCommentThrowTagSniff.php', 'Squiz_Sniffs_Commenting_InlineCommentSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/Commenting/InlineCommentSniff.php', 'Squiz_Sniffs_Commenting_LongConditionClosingCommentSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/Commenting/LongConditionClosingCommentSniff.php', 'Squiz_Sniffs_Commenting_PostStatementCommentSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/Commenting/PostStatementCommentSniff.php', 'Squiz_Sniffs_Commenting_VariableCommentSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/Commenting/VariableCommentSniff.php', 'Squiz_Sniffs_ControlStructures_ControlSignatureSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/ControlStructures/ControlSignatureSniff.php', 'Squiz_Sniffs_ControlStructures_ElseIfDeclarationSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/ControlStructures/ElseIfDeclarationSniff.php', 'Squiz_Sniffs_ControlStructures_ForEachLoopDeclarationSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/ControlStructures/ForEachLoopDeclarationSniff.php', 'Squiz_Sniffs_ControlStructures_ForLoopDeclarationSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/ControlStructures/ForLoopDeclarationSniff.php', 'Squiz_Sniffs_ControlStructures_InlineIfDeclarationSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/ControlStructures/InlineIfDeclarationSniff.php', 'Squiz_Sniffs_ControlStructures_LowercaseDeclarationSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/ControlStructures/LowercaseDeclarationSniff.php', 'Squiz_Sniffs_ControlStructures_SwitchDeclarationSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/ControlStructures/SwitchDeclarationSniff.php', 'Squiz_Sniffs_Debug_JSLintSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/Debug/JSLintSniff.php', 'Squiz_Sniffs_Debug_JavaScriptLintSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/Debug/JavaScriptLintSniff.php', 'Squiz_Sniffs_Files_FileExtensionSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/Files/FileExtensionSniff.php', 'Squiz_Sniffs_Formatting_OperatorBracketSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/Formatting/OperatorBracketSniff.php', 'Squiz_Sniffs_Functions_FunctionDeclarationArgumentSpacingSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/Functions/FunctionDeclarationArgumentSpacingSniff.php', 'Squiz_Sniffs_Functions_FunctionDeclarationSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/Functions/FunctionDeclarationSniff.php', 'Squiz_Sniffs_Functions_FunctionDuplicateArgumentSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/Functions/FunctionDuplicateArgumentSniff.php', 'Squiz_Sniffs_Functions_GlobalFunctionSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/Functions/GlobalFunctionSniff.php', 'Squiz_Sniffs_Functions_LowercaseFunctionKeywordsSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/Functions/LowercaseFunctionKeywordsSniff.php', 'Squiz_Sniffs_Functions_MultiLineFunctionDeclarationSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/Functions/MultiLineFunctionDeclarationSniff.php', 'Squiz_Sniffs_NamingConventions_ValidFunctionNameSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/NamingConventions/ValidFunctionNameSniff.php', 'Squiz_Sniffs_NamingConventions_ValidVariableNameSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/NamingConventions/ValidVariableNameSniff.php', 'Squiz_Sniffs_Objects_DisallowObjectStringIndexSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/Objects/DisallowObjectStringIndexSniff.php', 'Squiz_Sniffs_Objects_ObjectInstantiationSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/Objects/ObjectInstantiationSniff.php', 'Squiz_Sniffs_Objects_ObjectMemberCommaSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/Objects/ObjectMemberCommaSniff.php', 'Squiz_Sniffs_Operators_ComparisonOperatorUsageSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/Operators/ComparisonOperatorUsageSniff.php', 'Squiz_Sniffs_Operators_IncrementDecrementUsageSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/Operators/IncrementDecrementUsageSniff.php', 'Squiz_Sniffs_Operators_ValidLogicalOperatorsSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/Operators/ValidLogicalOperatorsSniff.php', 'Squiz_Sniffs_PHP_CommentedOutCodeSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/PHP/CommentedOutCodeSniff.php', 'Squiz_Sniffs_PHP_DisallowBooleanStatementSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/PHP/DisallowBooleanStatementSniff.php', 'Squiz_Sniffs_PHP_DisallowComparisonAssignmentSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/PHP/DisallowComparisonAssignmentSniff.php', 'Squiz_Sniffs_PHP_DisallowInlineIfSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/PHP/DisallowInlineIfSniff.php', 'Squiz_Sniffs_PHP_DisallowMultipleAssignmentsSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/PHP/DisallowMultipleAssignmentsSniff.php', 'Squiz_Sniffs_PHP_DisallowObEndFlushSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/PHP/DisallowObEndFlushSniff.php', 'Squiz_Sniffs_PHP_DisallowSizeFunctionsInLoopsSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/PHP/DisallowSizeFunctionsInLoopsSniff.php', 'Squiz_Sniffs_PHP_DiscouragedFunctionsSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/PHP/DiscouragedFunctionsSniff.php', 'Squiz_Sniffs_PHP_EmbeddedPhpSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/PHP/EmbeddedPhpSniff.php', 'Squiz_Sniffs_PHP_EvalSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/PHP/EvalSniff.php', 'Squiz_Sniffs_PHP_ForbiddenFunctionsSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/PHP/ForbiddenFunctionsSniff.php', 'Squiz_Sniffs_PHP_GlobalKeywordSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/PHP/GlobalKeywordSniff.php', 'Squiz_Sniffs_PHP_HeredocSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/PHP/HeredocSniff.php', 'Squiz_Sniffs_PHP_InnerFunctionsSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/PHP/InnerFunctionsSniff.php', 'Squiz_Sniffs_PHP_LowercasePHPFunctionsSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/PHP/LowercasePHPFunctionsSniff.php', 'Squiz_Sniffs_PHP_NonExecutableCodeSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/PHP/NonExecutableCodeSniff.php', 'Squiz_Sniffs_Scope_MemberVarScopeSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/Scope/MemberVarScopeSniff.php', 'Squiz_Sniffs_Scope_MethodScopeSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/Scope/MethodScopeSniff.php', 'Squiz_Sniffs_Scope_StaticThisUsageSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/Scope/StaticThisUsageSniff.php', 'Squiz_Sniffs_Strings_ConcatenationSpacingSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/Strings/ConcatenationSpacingSniff.php', 'Squiz_Sniffs_Strings_DoubleQuoteUsageSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/Strings/DoubleQuoteUsageSniff.php', 'Squiz_Sniffs_Strings_EchoedStringsSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/Strings/EchoedStringsSniff.php', 'Squiz_Sniffs_WhiteSpace_CastSpacingSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/WhiteSpace/CastSpacingSniff.php', 'Squiz_Sniffs_WhiteSpace_ControlStructureSpacingSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/WhiteSpace/ControlStructureSpacingSniff.php', 'Squiz_Sniffs_WhiteSpace_FunctionClosingBraceSpaceSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/WhiteSpace/FunctionClosingBraceSpaceSniff.php', 'Squiz_Sniffs_WhiteSpace_FunctionOpeningBraceSpaceSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/WhiteSpace/FunctionOpeningBraceSpaceSniff.php', 'Squiz_Sniffs_WhiteSpace_FunctionSpacingSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/WhiteSpace/FunctionSpacingSniff.php', 'Squiz_Sniffs_WhiteSpace_LanguageConstructSpacingSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/WhiteSpace/LanguageConstructSpacingSniff.php', 'Squiz_Sniffs_WhiteSpace_LogicalOperatorSpacingSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/WhiteSpace/LogicalOperatorSpacingSniff.php', 'Squiz_Sniffs_WhiteSpace_MemberVarSpacingSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/WhiteSpace/MemberVarSpacingSniff.php', 'Squiz_Sniffs_WhiteSpace_ObjectOperatorSpacingSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/WhiteSpace/ObjectOperatorSpacingSniff.php', 'Squiz_Sniffs_WhiteSpace_OperatorSpacingSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/WhiteSpace/OperatorSpacingSniff.php', 'Squiz_Sniffs_WhiteSpace_PropertyLabelSpacingSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/WhiteSpace/PropertyLabelSpacingSniff.php', 'Squiz_Sniffs_WhiteSpace_ScopeClosingBraceSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/WhiteSpace/ScopeClosingBraceSniff.php', 'Squiz_Sniffs_WhiteSpace_ScopeKeywordSpacingSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/WhiteSpace/ScopeKeywordSpacingSniff.php', 'Squiz_Sniffs_WhiteSpace_SemicolonSpacingSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/WhiteSpace/SemicolonSpacingSniff.php', 'Squiz_Sniffs_WhiteSpace_SuperfluousWhitespaceSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/WhiteSpace/SuperfluousWhitespaceSniff.php', 'Text_Template' => $vendorDir . '/phpunit/php-text-template/src/Template.php', 'Zend_Sniffs_Debug_CodeAnalyzerSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Zend/Sniffs/Debug/CodeAnalyzerSniff.php', 'Zend_Sniffs_Files_ClosingTagSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Zend/Sniffs/Files/ClosingTagSniff.php', 'Zend_Sniffs_NamingConventions_ValidVariableNameSniff' => $vendorDir . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Zend/Sniffs/NamingConventions/ValidVariableNameSniff.php', ); $vendorDir . '/symfony/polyfill-ctype/bootstrap.php', '0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => $vendorDir . '/symfony/polyfill-mbstring/bootstrap.php', 'e88992873b7765f9b5710cab95ba5dd7' => $vendorDir . '/hoa/consistency/Prelude.php', '3e76f7f02b41af8cea96018933f6b7e3' => $vendorDir . '/hoa/protocol/Wrapper.php', '6124b4c8570aa390c21fafd04a26c69f' => $vendorDir . '/myclabs/deep-copy/src/DeepCopy/deep_copy.php', '7b11c4dc42b3b3023073cb14e519683c' => $vendorDir . '/ralouphie/getallheaders/src/getallheaders.php', 'c964ee0ededf28c96ebd9db5099ef910' => $vendorDir . '/guzzlehttp/promises/src/functions_include.php', 'a0edc8309cc5e1d60e3047b5df6b7052' => $vendorDir . '/guzzlehttp/psr7/src/functions_include.php', '37a3dc5111fe8f707ab4c132ef1dbc62' => $vendorDir . '/guzzlehttp/guzzle/src/functions_include.php', ); array($vendorDir . '/phpoption/phpoption/src'), 'Flow\\JSONPath\\Test' => array($vendorDir . '/flow/jsonpath/tests'), 'Flow\\JSONPath' => array($vendorDir . '/flow/jsonpath/src'), 'Codeception\\' => array($vendorDir . '/codeception/specify/src'), 'Behat\\Gherkin' => array($vendorDir . '/behat/gherkin/src'), ); array($vendorDir . '/phpdocumentor/reflection-common/src', $vendorDir . '/phpdocumentor/reflection-docblock/src', $vendorDir . '/phpdocumentor/type-resolver/src'), 'Webmozart\\Assert\\' => array($vendorDir . '/webmozart/assert/src'), 'Symfony\\Polyfill\\Mbstring\\' => array($vendorDir . '/symfony/polyfill-mbstring'), 'Symfony\\Polyfill\\Ctype\\' => array($vendorDir . '/symfony/polyfill-ctype'), 'Symfony\\Component\\Yaml\\' => array($vendorDir . '/symfony/yaml'), 'Symfony\\Component\\Process\\' => array($vendorDir . '/symfony/process'), 'Symfony\\Component\\Finder\\' => array($vendorDir . '/symfony/finder'), 'Symfony\\Component\\EventDispatcher\\' => array($vendorDir . '/symfony/event-dispatcher'), 'Symfony\\Component\\DomCrawler\\' => array($vendorDir . '/symfony/dom-crawler'), 'Symfony\\Component\\Debug\\' => array($vendorDir . '/symfony/debug'), 'Symfony\\Component\\CssSelector\\' => array($vendorDir . '/symfony/css-selector'), 'Symfony\\Component\\Console\\' => array($vendorDir . '/symfony/console'), 'Symfony\\Component\\BrowserKit\\' => array($vendorDir . '/symfony/browser-kit'), 'Psr\\Log\\' => array($vendorDir . '/psr/log/Psr/Log'), 'Psr\\Http\\Message\\' => array($vendorDir . '/psr/http-message/src'), 'Prophecy\\' => array($vendorDir . '/phpspec/prophecy/src/Prophecy'), 'Predis\\' => array($vendorDir . '/predis/predis/src'), 'PhpAmqpLib\\' => array($vendorDir . '/php-amqplib/php-amqplib/PhpAmqpLib'), 'Pheanstalk\\' => array($vendorDir . '/pda/pheanstalk/src'), 'Monolog\\' => array($vendorDir . '/monolog/monolog/src/Monolog'), 'Hoa\\Ustring\\' => array($vendorDir . '/hoa/ustring'), 'Hoa\\Stream\\' => array($vendorDir . '/hoa/stream'), 'Hoa\\Protocol\\' => array($vendorDir . '/hoa/protocol'), 'Hoa\\Iterator\\' => array($vendorDir . '/hoa/iterator'), 'Hoa\\File\\' => array($vendorDir . '/hoa/file'), 'Hoa\\Exception\\' => array($vendorDir . '/hoa/exception'), 'Hoa\\Event\\' => array($vendorDir . '/hoa/event'), 'Hoa\\Console\\' => array($vendorDir . '/hoa/console'), 'Hoa\\Consistency\\' => array($vendorDir . '/hoa/consistency'), 'GuzzleHttp\\Psr7\\' => array($vendorDir . '/guzzlehttp/psr7/src'), 'GuzzleHttp\\Promise\\' => array($vendorDir . '/guzzlehttp/promises/src'), 'GuzzleHttp\\' => array($vendorDir . '/guzzlehttp/guzzle/src'), 'Facebook\\WebDriver\\' => array($vendorDir . '/facebook/webdriver/lib'), 'Dotenv\\' => array($vendorDir . '/vlucas/phpdotenv/src'), 'Doctrine\\Instantiator\\' => array($vendorDir . '/doctrine/instantiator/src/Doctrine/Instantiator'), 'DeepCopy\\' => array($vendorDir . '/myclabs/deep-copy/src/DeepCopy'), 'Codeception\\PHPUnit\\' => array($vendorDir . '/codeception/phpunit-wrapper/src'), 'Codeception\\Extension\\' => array($baseDir . '/ext'), 'Codeception\\' => array($baseDir . '/src/Codeception', $vendorDir . '/codeception/stub/src'), ); = 50600 && !defined('HHVM_VERSION') && (!function_exists('zend_loader_file_encoded') || !zend_loader_file_encoded()); if ($useStaticLoader) { require_once __DIR__ . '/autoload_static.php'; call_user_func(\Composer\Autoload\ComposerStaticInite0bcc21ff65691a5e7c90d59ffab2d23::getInitializer($loader)); } else { $map = require __DIR__ . '/autoload_namespaces.php'; foreach ($map as $namespace => $path) { $loader->set($namespace, $path); } $map = require __DIR__ . '/autoload_psr4.php'; foreach ($map as $namespace => $path) { $loader->setPsr4($namespace, $path); } $classMap = require __DIR__ . '/autoload_classmap.php'; if ($classMap) { $loader->addClassMap($classMap); } } $loader->register(true); if ($useStaticLoader) { $includeFiles = Composer\Autoload\ComposerStaticInite0bcc21ff65691a5e7c90d59ffab2d23::$files; } else { $includeFiles = require __DIR__ . '/autoload_files.php'; } foreach ($includeFiles as $fileIdentifier => $file) { composerRequiree0bcc21ff65691a5e7c90d59ffab2d23($fileIdentifier, $file); } return $loader; } } function composerRequiree0bcc21ff65691a5e7c90d59ffab2d23($fileIdentifier, $file) { if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) { require $file; $GLOBALS['__composer_autoload_files'][$fileIdentifier] = true; } } __DIR__ . '/..' . '/symfony/polyfill-ctype/bootstrap.php', '0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => __DIR__ . '/..' . '/symfony/polyfill-mbstring/bootstrap.php', 'e88992873b7765f9b5710cab95ba5dd7' => __DIR__ . '/..' . '/hoa/consistency/Prelude.php', '3e76f7f02b41af8cea96018933f6b7e3' => __DIR__ . '/..' . '/hoa/protocol/Wrapper.php', '6124b4c8570aa390c21fafd04a26c69f' => __DIR__ . '/..' . '/myclabs/deep-copy/src/DeepCopy/deep_copy.php', '7b11c4dc42b3b3023073cb14e519683c' => __DIR__ . '/..' . '/ralouphie/getallheaders/src/getallheaders.php', 'c964ee0ededf28c96ebd9db5099ef910' => __DIR__ . '/..' . '/guzzlehttp/promises/src/functions_include.php', 'a0edc8309cc5e1d60e3047b5df6b7052' => __DIR__ . '/..' . '/guzzlehttp/psr7/src/functions_include.php', '37a3dc5111fe8f707ab4c132ef1dbc62' => __DIR__ . '/..' . '/guzzlehttp/guzzle/src/functions_include.php', ); public static $prefixLengthsPsr4 = array ( 'p' => array ( 'phpDocumentor\\Reflection\\' => 25, ), 'W' => array ( 'Webmozart\\Assert\\' => 17, ), 'S' => array ( 'Symfony\\Polyfill\\Mbstring\\' => 26, 'Symfony\\Polyfill\\Ctype\\' => 23, 'Symfony\\Component\\Yaml\\' => 23, 'Symfony\\Component\\Process\\' => 26, 'Symfony\\Component\\Finder\\' => 25, 'Symfony\\Component\\EventDispatcher\\' => 34, 'Symfony\\Component\\DomCrawler\\' => 29, 'Symfony\\Component\\Debug\\' => 24, 'Symfony\\Component\\CssSelector\\' => 30, 'Symfony\\Component\\Console\\' => 26, 'Symfony\\Component\\BrowserKit\\' => 29, ), 'P' => array ( 'Psr\\Log\\' => 8, 'Psr\\Http\\Message\\' => 17, 'Prophecy\\' => 9, 'Predis\\' => 7, 'PhpAmqpLib\\' => 11, 'Pheanstalk\\' => 11, ), 'M' => array ( 'Monolog\\' => 8, ), 'H' => array ( 'Hoa\\Ustring\\' => 12, 'Hoa\\Stream\\' => 11, 'Hoa\\Protocol\\' => 13, 'Hoa\\Iterator\\' => 13, 'Hoa\\File\\' => 9, 'Hoa\\Exception\\' => 14, 'Hoa\\Event\\' => 10, 'Hoa\\Console\\' => 12, 'Hoa\\Consistency\\' => 16, ), 'G' => array ( 'GuzzleHttp\\Psr7\\' => 16, 'GuzzleHttp\\Promise\\' => 19, 'GuzzleHttp\\' => 11, ), 'F' => array ( 'Facebook\\WebDriver\\' => 19, ), 'D' => array ( 'Dotenv\\' => 7, 'Doctrine\\Instantiator\\' => 22, 'DeepCopy\\' => 9, ), 'C' => array ( 'Codeception\\PHPUnit\\' => 20, 'Codeception\\Extension\\' => 22, 'Codeception\\' => 12, ), ); public static $prefixDirsPsr4 = array ( 'phpDocumentor\\Reflection\\' => array ( 0 => __DIR__ . '/..' . '/phpdocumentor/reflection-common/src', 1 => __DIR__ . '/..' . '/phpdocumentor/reflection-docblock/src', 2 => __DIR__ . '/..' . '/phpdocumentor/type-resolver/src', ), 'Webmozart\\Assert\\' => array ( 0 => __DIR__ . '/..' . '/webmozart/assert/src', ), 'Symfony\\Polyfill\\Mbstring\\' => array ( 0 => __DIR__ . '/..' . '/symfony/polyfill-mbstring', ), 'Symfony\\Polyfill\\Ctype\\' => array ( 0 => __DIR__ . '/..' . '/symfony/polyfill-ctype', ), 'Symfony\\Component\\Yaml\\' => array ( 0 => __DIR__ . '/..' . '/symfony/yaml', ), 'Symfony\\Component\\Process\\' => array ( 0 => __DIR__ . '/..' . '/symfony/process', ), 'Symfony\\Component\\Finder\\' => array ( 0 => __DIR__ . '/..' . '/symfony/finder', ), 'Symfony\\Component\\EventDispatcher\\' => array ( 0 => __DIR__ . '/..' . '/symfony/event-dispatcher', ), 'Symfony\\Component\\DomCrawler\\' => array ( 0 => __DIR__ . '/..' . '/symfony/dom-crawler', ), 'Symfony\\Component\\Debug\\' => array ( 0 => __DIR__ . '/..' . '/symfony/debug', ), 'Symfony\\Component\\CssSelector\\' => array ( 0 => __DIR__ . '/..' . '/symfony/css-selector', ), 'Symfony\\Component\\Console\\' => array ( 0 => __DIR__ . '/..' . '/symfony/console', ), 'Symfony\\Component\\BrowserKit\\' => array ( 0 => __DIR__ . '/..' . '/symfony/browser-kit', ), 'Psr\\Log\\' => array ( 0 => __DIR__ . '/..' . '/psr/log/Psr/Log', ), 'Psr\\Http\\Message\\' => array ( 0 => __DIR__ . '/..' . '/psr/http-message/src', ), 'Prophecy\\' => array ( 0 => __DIR__ . '/..' . '/phpspec/prophecy/src/Prophecy', ), 'Predis\\' => array ( 0 => __DIR__ . '/..' . '/predis/predis/src', ), 'PhpAmqpLib\\' => array ( 0 => __DIR__ . '/..' . '/php-amqplib/php-amqplib/PhpAmqpLib', ), 'Pheanstalk\\' => array ( 0 => __DIR__ . '/..' . '/pda/pheanstalk/src', ), 'Monolog\\' => array ( 0 => __DIR__ . '/..' . '/monolog/monolog/src/Monolog', ), 'Hoa\\Ustring\\' => array ( 0 => __DIR__ . '/..' . '/hoa/ustring', ), 'Hoa\\Stream\\' => array ( 0 => __DIR__ . '/..' . '/hoa/stream', ), 'Hoa\\Protocol\\' => array ( 0 => __DIR__ . '/..' . '/hoa/protocol', ), 'Hoa\\Iterator\\' => array ( 0 => __DIR__ . '/..' . '/hoa/iterator', ), 'Hoa\\File\\' => array ( 0 => __DIR__ . '/..' . '/hoa/file', ), 'Hoa\\Exception\\' => array ( 0 => __DIR__ . '/..' . '/hoa/exception', ), 'Hoa\\Event\\' => array ( 0 => __DIR__ . '/..' . '/hoa/event', ), 'Hoa\\Console\\' => array ( 0 => __DIR__ . '/..' . '/hoa/console', ), 'Hoa\\Consistency\\' => array ( 0 => __DIR__ . '/..' . '/hoa/consistency', ), 'GuzzleHttp\\Psr7\\' => array ( 0 => __DIR__ . '/..' . '/guzzlehttp/psr7/src', ), 'GuzzleHttp\\Promise\\' => array ( 0 => __DIR__ . '/..' . '/guzzlehttp/promises/src', ), 'GuzzleHttp\\' => array ( 0 => __DIR__ . '/..' . '/guzzlehttp/guzzle/src', ), 'Facebook\\WebDriver\\' => array ( 0 => __DIR__ . '/..' . '/facebook/webdriver/lib', ), 'Dotenv\\' => array ( 0 => __DIR__ . '/..' . '/vlucas/phpdotenv/src', ), 'Doctrine\\Instantiator\\' => array ( 0 => __DIR__ . '/..' . '/doctrine/instantiator/src/Doctrine/Instantiator', ), 'DeepCopy\\' => array ( 0 => __DIR__ . '/..' . '/myclabs/deep-copy/src/DeepCopy', ), 'Codeception\\PHPUnit\\' => array ( 0 => __DIR__ . '/..' . '/codeception/phpunit-wrapper/src', ), 'Codeception\\Extension\\' => array ( 0 => __DIR__ . '/../..' . '/ext', ), 'Codeception\\' => array ( 0 => __DIR__ . '/../..' . '/src/Codeception', 1 => __DIR__ . '/..' . '/codeception/stub/src', ), ); public static $prefixesPsr0 = array ( 'P' => array ( 'PhpOption\\' => array ( 0 => __DIR__ . '/..' . '/phpoption/phpoption/src', ), ), 'F' => array ( 'Flow\\JSONPath\\Test' => array ( 0 => __DIR__ . '/..' . '/flow/jsonpath/tests', ), 'Flow\\JSONPath' => array ( 0 => __DIR__ . '/..' . '/flow/jsonpath/src', ), ), 'C' => array ( 'Codeception\\' => array ( 0 => __DIR__ . '/..' . '/codeception/specify/src', ), ), 'B' => array ( 'Behat\\Gherkin' => array ( 0 => __DIR__ . '/..' . '/behat/gherkin/src', ), ), ); public static $classMap = array ( 'File_Iterator' => __DIR__ . '/..' . '/phpunit/php-file-iterator/src/Iterator.php', 'File_Iterator_Facade' => __DIR__ . '/..' . '/phpunit/php-file-iterator/src/Facade.php', 'File_Iterator_Factory' => __DIR__ . '/..' . '/phpunit/php-file-iterator/src/Factory.php', 'Generic_Sniffs_Arrays_DisallowLongArraySyntaxSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Generic/Sniffs/Arrays/DisallowLongArraySyntaxSniff.php', 'Generic_Sniffs_Arrays_DisallowShortArraySyntaxSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Generic/Sniffs/Arrays/DisallowShortArraySyntaxSniff.php', 'Generic_Sniffs_Classes_DuplicateClassNameSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Generic/Sniffs/Classes/DuplicateClassNameSniff.php', 'Generic_Sniffs_Classes_OpeningBraceSameLineSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Generic/Sniffs/Classes/OpeningBraceSameLineSniff.php', 'Generic_Sniffs_CodeAnalysis_EmptyStatementSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Generic/Sniffs/CodeAnalysis/EmptyStatementSniff.php', 'Generic_Sniffs_CodeAnalysis_ForLoopShouldBeWhileLoopSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Generic/Sniffs/CodeAnalysis/ForLoopShouldBeWhileLoopSniff.php', 'Generic_Sniffs_CodeAnalysis_ForLoopWithTestFunctionCallSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Generic/Sniffs/CodeAnalysis/ForLoopWithTestFunctionCallSniff.php', 'Generic_Sniffs_CodeAnalysis_JumbledIncrementerSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Generic/Sniffs/CodeAnalysis/JumbledIncrementerSniff.php', 'Generic_Sniffs_CodeAnalysis_UnconditionalIfStatementSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Generic/Sniffs/CodeAnalysis/UnconditionalIfStatementSniff.php', 'Generic_Sniffs_CodeAnalysis_UnnecessaryFinalModifierSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Generic/Sniffs/CodeAnalysis/UnnecessaryFinalModifierSniff.php', 'Generic_Sniffs_CodeAnalysis_UnusedFunctionParameterSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Generic/Sniffs/CodeAnalysis/UnusedFunctionParameterSniff.php', 'Generic_Sniffs_CodeAnalysis_UselessOverridingMethodSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Generic/Sniffs/CodeAnalysis/UselessOverridingMethodSniff.php', 'Generic_Sniffs_Commenting_DocCommentSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Generic/Sniffs/Commenting/DocCommentSniff.php', 'Generic_Sniffs_Commenting_FixmeSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Generic/Sniffs/Commenting/FixmeSniff.php', 'Generic_Sniffs_Commenting_TodoSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Generic/Sniffs/Commenting/TodoSniff.php', 'Generic_Sniffs_ControlStructures_InlineControlStructureSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Generic/Sniffs/ControlStructures/InlineControlStructureSniff.php', 'Generic_Sniffs_Debug_CSSLintSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Generic/Sniffs/Debug/CSSLintSniff.php', 'Generic_Sniffs_Debug_ClosureLinterSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Generic/Sniffs/Debug/ClosureLinterSniff.php', 'Generic_Sniffs_Debug_ESLintSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Generic/Sniffs/Debug/ESLintSniff.php', 'Generic_Sniffs_Debug_JSHintSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Generic/Sniffs/Debug/JSHintSniff.php', 'Generic_Sniffs_Files_ByteOrderMarkSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Generic/Sniffs/Files/ByteOrderMarkSniff.php', 'Generic_Sniffs_Files_EndFileNewlineSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Generic/Sniffs/Files/EndFileNewlineSniff.php', 'Generic_Sniffs_Files_EndFileNoNewlineSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Generic/Sniffs/Files/EndFileNoNewlineSniff.php', 'Generic_Sniffs_Files_InlineHTMLSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Generic/Sniffs/Files/InlineHTMLSniff.php', 'Generic_Sniffs_Files_LineEndingsSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Generic/Sniffs/Files/LineEndingsSniff.php', 'Generic_Sniffs_Files_LineLengthSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Generic/Sniffs/Files/LineLengthSniff.php', 'Generic_Sniffs_Files_LowercasedFilenameSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Generic/Sniffs/Files/LowercasedFilenameSniff.php', 'Generic_Sniffs_Files_OneClassPerFileSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Generic/Sniffs/Files/OneClassPerFileSniff.php', 'Generic_Sniffs_Files_OneInterfacePerFileSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Generic/Sniffs/Files/OneInterfacePerFileSniff.php', 'Generic_Sniffs_Files_OneTraitPerFileSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Generic/Sniffs/Files/OneTraitPerFileSniff.php', 'Generic_Sniffs_Formatting_DisallowMultipleStatementsSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Generic/Sniffs/Formatting/DisallowMultipleStatementsSniff.php', 'Generic_Sniffs_Formatting_MultipleStatementAlignmentSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Generic/Sniffs/Formatting/MultipleStatementAlignmentSniff.php', 'Generic_Sniffs_Formatting_NoSpaceAfterCastSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Generic/Sniffs/Formatting/NoSpaceAfterCastSniff.php', 'Generic_Sniffs_Formatting_SpaceAfterCastSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Generic/Sniffs/Formatting/SpaceAfterCastSniff.php', 'Generic_Sniffs_Formatting_SpaceAfterNotSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Generic/Sniffs/Formatting/SpaceAfterNotSniff.php', 'Generic_Sniffs_Functions_CallTimePassByReferenceSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Generic/Sniffs/Functions/CallTimePassByReferenceSniff.php', 'Generic_Sniffs_Functions_FunctionCallArgumentSpacingSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Generic/Sniffs/Functions/FunctionCallArgumentSpacingSniff.php', 'Generic_Sniffs_Functions_OpeningFunctionBraceBsdAllmanSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Generic/Sniffs/Functions/OpeningFunctionBraceBsdAllmanSniff.php', 'Generic_Sniffs_Functions_OpeningFunctionBraceKernighanRitchieSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Generic/Sniffs/Functions/OpeningFunctionBraceKernighanRitchieSniff.php', 'Generic_Sniffs_Metrics_CyclomaticComplexitySniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Generic/Sniffs/Metrics/CyclomaticComplexitySniff.php', 'Generic_Sniffs_Metrics_NestingLevelSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Generic/Sniffs/Metrics/NestingLevelSniff.php', 'Generic_Sniffs_NamingConventions_CamelCapsFunctionNameSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Generic/Sniffs/NamingConventions/CamelCapsFunctionNameSniff.php', 'Generic_Sniffs_NamingConventions_ConstructorNameSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Generic/Sniffs/NamingConventions/ConstructorNameSniff.php', 'Generic_Sniffs_NamingConventions_UpperCaseConstantNameSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Generic/Sniffs/NamingConventions/UpperCaseConstantNameSniff.php', 'Generic_Sniffs_PHP_BacktickOperatorSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Generic/Sniffs/PHP/BacktickOperatorSniff.php', 'Generic_Sniffs_PHP_CharacterBeforePHPOpeningTagSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Generic/Sniffs/PHP/CharacterBeforePHPOpeningTagSniff.php', 'Generic_Sniffs_PHP_ClosingPHPTagSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Generic/Sniffs/PHP/ClosingPHPTagSniff.php', 'Generic_Sniffs_PHP_DeprecatedFunctionsSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Generic/Sniffs/PHP/DeprecatedFunctionsSniff.php', 'Generic_Sniffs_PHP_DisallowAlternativePHPTagsSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Generic/Sniffs/PHP/DisallowAlternativePHPTagsSniff.php', 'Generic_Sniffs_PHP_DisallowShortOpenTagSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Generic/Sniffs/PHP/DisallowShortOpenTagSniff.php', 'Generic_Sniffs_PHP_ForbiddenFunctionsSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Generic/Sniffs/PHP/ForbiddenFunctionsSniff.php', 'Generic_Sniffs_PHP_LowerCaseConstantSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Generic/Sniffs/PHP/LowerCaseConstantSniff.php', 'Generic_Sniffs_PHP_LowerCaseKeywordSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Generic/Sniffs/PHP/LowerCaseKeywordSniff.php', 'Generic_Sniffs_PHP_NoSilencedErrorsSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Generic/Sniffs/PHP/NoSilencedErrorsSniff.php', 'Generic_Sniffs_PHP_SAPIUsageSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Generic/Sniffs/PHP/SAPIUsageSniff.php', 'Generic_Sniffs_PHP_SyntaxSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Generic/Sniffs/PHP/SyntaxSniff.php', 'Generic_Sniffs_PHP_UpperCaseConstantSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Generic/Sniffs/PHP/UpperCaseConstantSniff.php', 'Generic_Sniffs_Strings_UnnecessaryStringConcatSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Generic/Sniffs/Strings/UnnecessaryStringConcatSniff.php', 'Generic_Sniffs_VersionControl_SubversionPropertiesSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Generic/Sniffs/VersionControl/SubversionPropertiesSniff.php', 'Generic_Sniffs_WhiteSpace_DisallowSpaceIndentSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Generic/Sniffs/WhiteSpace/DisallowSpaceIndentSniff.php', 'Generic_Sniffs_WhiteSpace_DisallowTabIndentSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Generic/Sniffs/WhiteSpace/DisallowTabIndentSniff.php', 'Generic_Sniffs_WhiteSpace_ScopeIndentSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Generic/Sniffs/WhiteSpace/ScopeIndentSniff.php', 'MySource_Sniffs_CSS_BrowserSpecificStylesSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/MySource/Sniffs/CSS/BrowserSpecificStylesSniff.php', 'MySource_Sniffs_Channels_DisallowSelfActionsSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/MySource/Sniffs/Channels/DisallowSelfActionsSniff.php', 'MySource_Sniffs_Channels_IncludeOwnSystemSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/MySource/Sniffs/Channels/IncludeOwnSystemSniff.php', 'MySource_Sniffs_Channels_IncludeSystemSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/MySource/Sniffs/Channels/IncludeSystemSniff.php', 'MySource_Sniffs_Channels_UnusedSystemSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/MySource/Sniffs/Channels/UnusedSystemSniff.php', 'MySource_Sniffs_Commenting_FunctionCommentSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/MySource/Sniffs/Commenting/FunctionCommentSniff.php', 'MySource_Sniffs_Debug_DebugCodeSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/MySource/Sniffs/Debug/DebugCodeSniff.php', 'MySource_Sniffs_Debug_FirebugConsoleSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/MySource/Sniffs/Debug/FirebugConsoleSniff.php', 'MySource_Sniffs_Objects_AssignThisSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/MySource/Sniffs/Objects/AssignThisSniff.php', 'MySource_Sniffs_Objects_CreateWidgetTypeCallbackSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/MySource/Sniffs/Objects/CreateWidgetTypeCallbackSniff.php', 'MySource_Sniffs_Objects_DisallowNewWidgetSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/MySource/Sniffs/Objects/DisallowNewWidgetSniff.php', 'MySource_Sniffs_PHP_AjaxNullComparisonSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/MySource/Sniffs/PHP/AjaxNullComparisonSniff.php', 'MySource_Sniffs_PHP_EvalObjectFactorySniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/MySource/Sniffs/PHP/EvalObjectFactorySniff.php', 'MySource_Sniffs_PHP_GetRequestDataSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/MySource/Sniffs/PHP/GetRequestDataSniff.php', 'MySource_Sniffs_PHP_ReturnFunctionValueSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/MySource/Sniffs/PHP/ReturnFunctionValueSniff.php', 'MySource_Sniffs_Strings_JoinStringsSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/MySource/Sniffs/Strings/JoinStringsSniff.php', 'PEAR_Sniffs_Classes_ClassDeclarationSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/PEAR/Sniffs/Classes/ClassDeclarationSniff.php', 'PEAR_Sniffs_Commenting_ClassCommentSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/PEAR/Sniffs/Commenting/ClassCommentSniff.php', 'PEAR_Sniffs_Commenting_FileCommentSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/PEAR/Sniffs/Commenting/FileCommentSniff.php', 'PEAR_Sniffs_Commenting_FunctionCommentSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/PEAR/Sniffs/Commenting/FunctionCommentSniff.php', 'PEAR_Sniffs_Commenting_InlineCommentSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/PEAR/Sniffs/Commenting/InlineCommentSniff.php', 'PEAR_Sniffs_ControlStructures_ControlSignatureSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/PEAR/Sniffs/ControlStructures/ControlSignatureSniff.php', 'PEAR_Sniffs_ControlStructures_MultiLineConditionSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/PEAR/Sniffs/ControlStructures/MultiLineConditionSniff.php', 'PEAR_Sniffs_Files_IncludingFileSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/PEAR/Sniffs/Files/IncludingFileSniff.php', 'PEAR_Sniffs_Formatting_MultiLineAssignmentSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/PEAR/Sniffs/Formatting/MultiLineAssignmentSniff.php', 'PEAR_Sniffs_Functions_FunctionCallSignatureSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/PEAR/Sniffs/Functions/FunctionCallSignatureSniff.php', 'PEAR_Sniffs_Functions_FunctionDeclarationSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/PEAR/Sniffs/Functions/FunctionDeclarationSniff.php', 'PEAR_Sniffs_Functions_ValidDefaultValueSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/PEAR/Sniffs/Functions/ValidDefaultValueSniff.php', 'PEAR_Sniffs_NamingConventions_ValidClassNameSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/PEAR/Sniffs/NamingConventions/ValidClassNameSniff.php', 'PEAR_Sniffs_NamingConventions_ValidFunctionNameSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/PEAR/Sniffs/NamingConventions/ValidFunctionNameSniff.php', 'PEAR_Sniffs_NamingConventions_ValidVariableNameSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/PEAR/Sniffs/NamingConventions/ValidVariableNameSniff.php', 'PEAR_Sniffs_WhiteSpace_ObjectOperatorIndentSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/PEAR/Sniffs/WhiteSpace/ObjectOperatorIndentSniff.php', 'PEAR_Sniffs_WhiteSpace_ScopeClosingBraceSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/PEAR/Sniffs/WhiteSpace/ScopeClosingBraceSniff.php', 'PEAR_Sniffs_WhiteSpace_ScopeIndentSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/PEAR/Sniffs/WhiteSpace/ScopeIndentSniff.php', 'PHPUnit\\Framework\\Assert' => __DIR__ . '/..' . '/phpunit/phpunit/src/ForwardCompatibility/Assert.php', 'PHPUnit\\Framework\\AssertionFailedError' => __DIR__ . '/..' . '/phpunit/phpunit/src/ForwardCompatibility/AssertionFailedError.php', 'PHPUnit\\Framework\\BaseTestListener' => __DIR__ . '/..' . '/phpunit/phpunit/src/ForwardCompatibility/BaseTestListener.php', 'PHPUnit\\Framework\\Test' => __DIR__ . '/..' . '/phpunit/phpunit/src/ForwardCompatibility/Test.php', 'PHPUnit\\Framework\\TestCase' => __DIR__ . '/..' . '/phpunit/phpunit/src/ForwardCompatibility/TestCase.php', 'PHPUnit\\Framework\\TestListener' => __DIR__ . '/..' . '/phpunit/phpunit/src/ForwardCompatibility/TestListener.php', 'PHPUnit\\Framework\\TestSuite' => __DIR__ . '/..' . '/phpunit/phpunit/src/ForwardCompatibility/TestSuite.php', 'PHPUnit_Exception' => __DIR__ . '/..' . '/phpunit/phpunit/src/Exception.php', 'PHPUnit_Extensions_GroupTestSuite' => __DIR__ . '/..' . '/phpunit/phpunit/src/Extensions/GroupTestSuite.php', 'PHPUnit_Extensions_PhptTestCase' => __DIR__ . '/..' . '/phpunit/phpunit/src/Extensions/PhptTestCase.php', 'PHPUnit_Extensions_PhptTestSuite' => __DIR__ . '/..' . '/phpunit/phpunit/src/Extensions/PhptTestSuite.php', 'PHPUnit_Extensions_RepeatedTest' => __DIR__ . '/..' . '/phpunit/phpunit/src/Extensions/RepeatedTest.php', 'PHPUnit_Extensions_TestDecorator' => __DIR__ . '/..' . '/phpunit/phpunit/src/Extensions/TestDecorator.php', 'PHPUnit_Extensions_TicketListener' => __DIR__ . '/..' . '/phpunit/phpunit/src/Extensions/TicketListener.php', 'PHPUnit_Framework_Assert' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/Assert.php', 'PHPUnit_Framework_AssertionFailedError' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/AssertionFailedError.php', 'PHPUnit_Framework_BaseTestListener' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/BaseTestListener.php', 'PHPUnit_Framework_CodeCoverageException' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/CodeCoverageException.php', 'PHPUnit_Framework_Constraint' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/Constraint.php', 'PHPUnit_Framework_Constraint_And' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/Constraint/And.php', 'PHPUnit_Framework_Constraint_ArrayHasKey' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/Constraint/ArrayHasKey.php', 'PHPUnit_Framework_Constraint_ArraySubset' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/Constraint/ArraySubset.php', 'PHPUnit_Framework_Constraint_Attribute' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/Constraint/Attribute.php', 'PHPUnit_Framework_Constraint_Callback' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/Constraint/Callback.php', 'PHPUnit_Framework_Constraint_ClassHasAttribute' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/Constraint/ClassHasAttribute.php', 'PHPUnit_Framework_Constraint_ClassHasStaticAttribute' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/Constraint/ClassHasStaticAttribute.php', 'PHPUnit_Framework_Constraint_Composite' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/Constraint/Composite.php', 'PHPUnit_Framework_Constraint_Count' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/Constraint/Count.php', 'PHPUnit_Framework_Constraint_DirectoryExists' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/Constraint/DirectoryExists.php', 'PHPUnit_Framework_Constraint_Exception' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/Constraint/Exception.php', 'PHPUnit_Framework_Constraint_ExceptionCode' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/Constraint/ExceptionCode.php', 'PHPUnit_Framework_Constraint_ExceptionMessage' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/Constraint/ExceptionMessage.php', 'PHPUnit_Framework_Constraint_ExceptionMessageRegExp' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/Constraint/ExceptionMessageRegExp.php', 'PHPUnit_Framework_Constraint_FileExists' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/Constraint/FileExists.php', 'PHPUnit_Framework_Constraint_GreaterThan' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/Constraint/GreaterThan.php', 'PHPUnit_Framework_Constraint_IsAnything' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/Constraint/IsAnything.php', 'PHPUnit_Framework_Constraint_IsEmpty' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/Constraint/IsEmpty.php', 'PHPUnit_Framework_Constraint_IsEqual' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/Constraint/IsEqual.php', 'PHPUnit_Framework_Constraint_IsFalse' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/Constraint/IsFalse.php', 'PHPUnit_Framework_Constraint_IsFinite' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/Constraint/IsFinite.php', 'PHPUnit_Framework_Constraint_IsIdentical' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/Constraint/IsIdentical.php', 'PHPUnit_Framework_Constraint_IsInfinite' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/Constraint/IsInfinite.php', 'PHPUnit_Framework_Constraint_IsInstanceOf' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/Constraint/IsInstanceOf.php', 'PHPUnit_Framework_Constraint_IsJson' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/Constraint/IsJson.php', 'PHPUnit_Framework_Constraint_IsNan' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/Constraint/IsNan.php', 'PHPUnit_Framework_Constraint_IsNull' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/Constraint/IsNull.php', 'PHPUnit_Framework_Constraint_IsReadable' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/Constraint/IsReadable.php', 'PHPUnit_Framework_Constraint_IsTrue' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/Constraint/IsTrue.php', 'PHPUnit_Framework_Constraint_IsType' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/Constraint/IsType.php', 'PHPUnit_Framework_Constraint_IsWritable' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/Constraint/IsWritable.php', 'PHPUnit_Framework_Constraint_JsonMatches' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/Constraint/JsonMatches.php', 'PHPUnit_Framework_Constraint_JsonMatches_ErrorMessageProvider' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/Constraint/JsonMatches/ErrorMessageProvider.php', 'PHPUnit_Framework_Constraint_LessThan' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/Constraint/LessThan.php', 'PHPUnit_Framework_Constraint_Not' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/Constraint/Not.php', 'PHPUnit_Framework_Constraint_ObjectHasAttribute' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/Constraint/ObjectHasAttribute.php', 'PHPUnit_Framework_Constraint_Or' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/Constraint/Or.php', 'PHPUnit_Framework_Constraint_PCREMatch' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/Constraint/PCREMatch.php', 'PHPUnit_Framework_Constraint_SameSize' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/Constraint/SameSize.php', 'PHPUnit_Framework_Constraint_StringContains' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/Constraint/StringContains.php', 'PHPUnit_Framework_Constraint_StringEndsWith' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/Constraint/StringEndsWith.php', 'PHPUnit_Framework_Constraint_StringMatches' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/Constraint/StringMatches.php', 'PHPUnit_Framework_Constraint_StringStartsWith' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/Constraint/StringStartsWith.php', 'PHPUnit_Framework_Constraint_TraversableContains' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/Constraint/TraversableContains.php', 'PHPUnit_Framework_Constraint_TraversableContainsOnly' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/Constraint/TraversableContainsOnly.php', 'PHPUnit_Framework_Constraint_Xor' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/Constraint/Xor.php', 'PHPUnit_Framework_CoveredCodeNotExecutedException' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/CoveredCodeNotExecutedException.php', 'PHPUnit_Framework_Error' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/Error.php', 'PHPUnit_Framework_Error_Deprecated' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/Error/Deprecated.php', 'PHPUnit_Framework_Error_Notice' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/Error/Notice.php', 'PHPUnit_Framework_Error_Warning' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/Error/Warning.php', 'PHPUnit_Framework_Exception' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/Exception.php', 'PHPUnit_Framework_ExceptionWrapper' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/ExceptionWrapper.php', 'PHPUnit_Framework_ExpectationFailedException' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/ExpectationFailedException.php', 'PHPUnit_Framework_IncompleteTest' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/IncompleteTest.php', 'PHPUnit_Framework_IncompleteTestCase' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/IncompleteTestCase.php', 'PHPUnit_Framework_IncompleteTestError' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/IncompleteTestError.php', 'PHPUnit_Framework_InvalidCoversTargetException' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/InvalidCoversTargetException.php', 'PHPUnit_Framework_MissingCoversAnnotationException' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/MissingCoversAnnotationException.php', 'PHPUnit_Framework_MockObject_BadMethodCallException' => __DIR__ . '/..' . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Exception/BadMethodCallException.php', 'PHPUnit_Framework_MockObject_Builder_Identity' => __DIR__ . '/..' . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Builder/Identity.php', 'PHPUnit_Framework_MockObject_Builder_InvocationMocker' => __DIR__ . '/..' . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Builder/InvocationMocker.php', 'PHPUnit_Framework_MockObject_Builder_Match' => __DIR__ . '/..' . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Builder/Match.php', 'PHPUnit_Framework_MockObject_Builder_MethodNameMatch' => __DIR__ . '/..' . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Builder/MethodNameMatch.php', 'PHPUnit_Framework_MockObject_Builder_Namespace' => __DIR__ . '/..' . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Builder/Namespace.php', 'PHPUnit_Framework_MockObject_Builder_ParametersMatch' => __DIR__ . '/..' . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Builder/ParametersMatch.php', 'PHPUnit_Framework_MockObject_Builder_Stub' => __DIR__ . '/..' . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Builder/Stub.php', 'PHPUnit_Framework_MockObject_Exception' => __DIR__ . '/..' . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Exception/Exception.php', 'PHPUnit_Framework_MockObject_Generator' => __DIR__ . '/..' . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Generator.php', 'PHPUnit_Framework_MockObject_Invocation' => __DIR__ . '/..' . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Invocation.php', 'PHPUnit_Framework_MockObject_InvocationMocker' => __DIR__ . '/..' . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/InvocationMocker.php', 'PHPUnit_Framework_MockObject_Invocation_Object' => __DIR__ . '/..' . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Invocation/Object.php', 'PHPUnit_Framework_MockObject_Invocation_Static' => __DIR__ . '/..' . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Invocation/Static.php', 'PHPUnit_Framework_MockObject_Invokable' => __DIR__ . '/..' . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Invokable.php', 'PHPUnit_Framework_MockObject_Matcher' => __DIR__ . '/..' . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Matcher.php', 'PHPUnit_Framework_MockObject_Matcher_AnyInvokedCount' => __DIR__ . '/..' . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Matcher/AnyInvokedCount.php', 'PHPUnit_Framework_MockObject_Matcher_AnyParameters' => __DIR__ . '/..' . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Matcher/AnyParameters.php', 'PHPUnit_Framework_MockObject_Matcher_ConsecutiveParameters' => __DIR__ . '/..' . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Matcher/ConsecutiveParameters.php', 'PHPUnit_Framework_MockObject_Matcher_Invocation' => __DIR__ . '/..' . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Matcher/Invocation.php', 'PHPUnit_Framework_MockObject_Matcher_InvokedAtIndex' => __DIR__ . '/..' . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Matcher/InvokedAtIndex.php', 'PHPUnit_Framework_MockObject_Matcher_InvokedAtLeastCount' => __DIR__ . '/..' . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Matcher/InvokedAtLeastCount.php', 'PHPUnit_Framework_MockObject_Matcher_InvokedAtLeastOnce' => __DIR__ . '/..' . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Matcher/InvokedAtLeastOnce.php', 'PHPUnit_Framework_MockObject_Matcher_InvokedAtMostCount' => __DIR__ . '/..' . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Matcher/InvokedAtMostCount.php', 'PHPUnit_Framework_MockObject_Matcher_InvokedCount' => __DIR__ . '/..' . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Matcher/InvokedCount.php', 'PHPUnit_Framework_MockObject_Matcher_InvokedRecorder' => __DIR__ . '/..' . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Matcher/InvokedRecorder.php', 'PHPUnit_Framework_MockObject_Matcher_MethodName' => __DIR__ . '/..' . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Matcher/MethodName.php', 'PHPUnit_Framework_MockObject_Matcher_Parameters' => __DIR__ . '/..' . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Matcher/Parameters.php', 'PHPUnit_Framework_MockObject_Matcher_StatelessInvocation' => __DIR__ . '/..' . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Matcher/StatelessInvocation.php', 'PHPUnit_Framework_MockObject_MockBuilder' => __DIR__ . '/..' . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/MockBuilder.php', 'PHPUnit_Framework_MockObject_MockObject' => __DIR__ . '/..' . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/MockObject.php', 'PHPUnit_Framework_MockObject_RuntimeException' => __DIR__ . '/..' . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Exception/RuntimeException.php', 'PHPUnit_Framework_MockObject_Stub' => __DIR__ . '/..' . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Stub.php', 'PHPUnit_Framework_MockObject_Stub_ConsecutiveCalls' => __DIR__ . '/..' . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Stub/ConsecutiveCalls.php', 'PHPUnit_Framework_MockObject_Stub_Exception' => __DIR__ . '/..' . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Stub/Exception.php', 'PHPUnit_Framework_MockObject_Stub_MatcherCollection' => __DIR__ . '/..' . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Stub/MatcherCollection.php', 'PHPUnit_Framework_MockObject_Stub_Return' => __DIR__ . '/..' . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Stub/Return.php', 'PHPUnit_Framework_MockObject_Stub_ReturnArgument' => __DIR__ . '/..' . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Stub/ReturnArgument.php', 'PHPUnit_Framework_MockObject_Stub_ReturnCallback' => __DIR__ . '/..' . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Stub/ReturnCallback.php', 'PHPUnit_Framework_MockObject_Stub_ReturnReference' => __DIR__ . '/..' . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Stub/ReturnReference.php', 'PHPUnit_Framework_MockObject_Stub_ReturnSelf' => __DIR__ . '/..' . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Stub/ReturnSelf.php', 'PHPUnit_Framework_MockObject_Stub_ReturnValueMap' => __DIR__ . '/..' . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Stub/ReturnValueMap.php', 'PHPUnit_Framework_MockObject_Verifiable' => __DIR__ . '/..' . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Verifiable.php', 'PHPUnit_Framework_OutputError' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/OutputError.php', 'PHPUnit_Framework_RiskyTest' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/RiskyTest.php', 'PHPUnit_Framework_RiskyTestError' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/RiskyTestError.php', 'PHPUnit_Framework_SelfDescribing' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/SelfDescribing.php', 'PHPUnit_Framework_SkippedTest' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/SkippedTest.php', 'PHPUnit_Framework_SkippedTestCase' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/SkippedTestCase.php', 'PHPUnit_Framework_SkippedTestError' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/SkippedTestError.php', 'PHPUnit_Framework_SkippedTestSuiteError' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/SkippedTestSuiteError.php', 'PHPUnit_Framework_SyntheticError' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/SyntheticError.php', 'PHPUnit_Framework_Test' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/Test.php', 'PHPUnit_Framework_TestCase' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/TestCase.php', 'PHPUnit_Framework_TestFailure' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/TestFailure.php', 'PHPUnit_Framework_TestListener' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/TestListener.php', 'PHPUnit_Framework_TestResult' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/TestResult.php', 'PHPUnit_Framework_TestSuite' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/TestSuite.php', 'PHPUnit_Framework_TestSuite_DataProvider' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/TestSuite/DataProvider.php', 'PHPUnit_Framework_UnintentionallyCoveredCodeError' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/UnintentionallyCoveredCodeError.php', 'PHPUnit_Framework_Warning' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/Warning.php', 'PHPUnit_Framework_WarningTestCase' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/WarningTestCase.php', 'PHPUnit_Runner_BaseTestRunner' => __DIR__ . '/..' . '/phpunit/phpunit/src/Runner/BaseTestRunner.php', 'PHPUnit_Runner_Exception' => __DIR__ . '/..' . '/phpunit/phpunit/src/Runner/Exception.php', 'PHPUnit_Runner_Filter_Factory' => __DIR__ . '/..' . '/phpunit/phpunit/src/Runner/Filter/Factory.php', 'PHPUnit_Runner_Filter_GroupFilterIterator' => __DIR__ . '/..' . '/phpunit/phpunit/src/Runner/Filter/Group.php', 'PHPUnit_Runner_Filter_Group_Exclude' => __DIR__ . '/..' . '/phpunit/phpunit/src/Runner/Filter/Group/Exclude.php', 'PHPUnit_Runner_Filter_Group_Include' => __DIR__ . '/..' . '/phpunit/phpunit/src/Runner/Filter/Group/Include.php', 'PHPUnit_Runner_Filter_Test' => __DIR__ . '/..' . '/phpunit/phpunit/src/Runner/Filter/Test.php', 'PHPUnit_Runner_StandardTestSuiteLoader' => __DIR__ . '/..' . '/phpunit/phpunit/src/Runner/StandardTestSuiteLoader.php', 'PHPUnit_Runner_TestSuiteLoader' => __DIR__ . '/..' . '/phpunit/phpunit/src/Runner/TestSuiteLoader.php', 'PHPUnit_Runner_Version' => __DIR__ . '/..' . '/phpunit/phpunit/src/Runner/Version.php', 'PHPUnit_TextUI_Command' => __DIR__ . '/..' . '/phpunit/phpunit/src/TextUI/Command.php', 'PHPUnit_TextUI_ResultPrinter' => __DIR__ . '/..' . '/phpunit/phpunit/src/TextUI/ResultPrinter.php', 'PHPUnit_TextUI_TestRunner' => __DIR__ . '/..' . '/phpunit/phpunit/src/TextUI/TestRunner.php', 'PHPUnit_Util_Blacklist' => __DIR__ . '/..' . '/phpunit/phpunit/src/Util/Blacklist.php', 'PHPUnit_Util_Configuration' => __DIR__ . '/..' . '/phpunit/phpunit/src/Util/Configuration.php', 'PHPUnit_Util_ConfigurationGenerator' => __DIR__ . '/..' . '/phpunit/phpunit/src/Util/ConfigurationGenerator.php', 'PHPUnit_Util_ErrorHandler' => __DIR__ . '/..' . '/phpunit/phpunit/src/Util/ErrorHandler.php', 'PHPUnit_Util_Fileloader' => __DIR__ . '/..' . '/phpunit/phpunit/src/Util/Fileloader.php', 'PHPUnit_Util_Filesystem' => __DIR__ . '/..' . '/phpunit/phpunit/src/Util/Filesystem.php', 'PHPUnit_Util_Filter' => __DIR__ . '/..' . '/phpunit/phpunit/src/Util/Filter.php', 'PHPUnit_Util_Getopt' => __DIR__ . '/..' . '/phpunit/phpunit/src/Util/Getopt.php', 'PHPUnit_Util_GlobalState' => __DIR__ . '/..' . '/phpunit/phpunit/src/Util/GlobalState.php', 'PHPUnit_Util_InvalidArgumentHelper' => __DIR__ . '/..' . '/phpunit/phpunit/src/Util/InvalidArgumentHelper.php', 'PHPUnit_Util_Log_JSON' => __DIR__ . '/..' . '/phpunit/phpunit/src/Util/Log/JSON.php', 'PHPUnit_Util_Log_JUnit' => __DIR__ . '/..' . '/phpunit/phpunit/src/Util/Log/JUnit.php', 'PHPUnit_Util_Log_TAP' => __DIR__ . '/..' . '/phpunit/phpunit/src/Util/Log/TAP.php', 'PHPUnit_Util_Log_TeamCity' => __DIR__ . '/..' . '/phpunit/phpunit/src/Util/Log/TeamCity.php', 'PHPUnit_Util_PHP' => __DIR__ . '/..' . '/phpunit/phpunit/src/Util/PHP.php', 'PHPUnit_Util_PHP_Default' => __DIR__ . '/..' . '/phpunit/phpunit/src/Util/PHP/Default.php', 'PHPUnit_Util_PHP_Windows' => __DIR__ . '/..' . '/phpunit/phpunit/src/Util/PHP/Windows.php', 'PHPUnit_Util_Printer' => __DIR__ . '/..' . '/phpunit/phpunit/src/Util/Printer.php', 'PHPUnit_Util_Regex' => __DIR__ . '/..' . '/phpunit/phpunit/src/Util/Regex.php', 'PHPUnit_Util_String' => __DIR__ . '/..' . '/phpunit/phpunit/src/Util/String.php', 'PHPUnit_Util_Test' => __DIR__ . '/..' . '/phpunit/phpunit/src/Util/Test.php', 'PHPUnit_Util_TestDox_NamePrettifier' => __DIR__ . '/..' . '/phpunit/phpunit/src/Util/TestDox/NamePrettifier.php', 'PHPUnit_Util_TestDox_ResultPrinter' => __DIR__ . '/..' . '/phpunit/phpunit/src/Util/TestDox/ResultPrinter.php', 'PHPUnit_Util_TestDox_ResultPrinter_HTML' => __DIR__ . '/..' . '/phpunit/phpunit/src/Util/TestDox/ResultPrinter/HTML.php', 'PHPUnit_Util_TestDox_ResultPrinter_Text' => __DIR__ . '/..' . '/phpunit/phpunit/src/Util/TestDox/ResultPrinter/Text.php', 'PHPUnit_Util_TestDox_ResultPrinter_XML' => __DIR__ . '/..' . '/phpunit/phpunit/src/Util/TestDox/ResultPrinter/XML.php', 'PHPUnit_Util_TestSuiteIterator' => __DIR__ . '/..' . '/phpunit/phpunit/src/Util/TestSuiteIterator.php', 'PHPUnit_Util_Type' => __DIR__ . '/..' . '/phpunit/phpunit/src/Util/Type.php', 'PHPUnit_Util_XML' => __DIR__ . '/..' . '/phpunit/phpunit/src/Util/XML.php', 'PHP_CodeSniffer' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer.php', 'PHP_CodeSniffer_CLI' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/CLI.php', 'PHP_CodeSniffer_DocGenerators_Generator' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/DocGenerators/Generator.php', 'PHP_CodeSniffer_DocGenerators_HTML' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/DocGenerators/HTML.php', 'PHP_CodeSniffer_DocGenerators_Markdown' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/DocGenerators/Markdown.php', 'PHP_CodeSniffer_DocGenerators_Text' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/DocGenerators/Text.php', 'PHP_CodeSniffer_Exception' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Exception.php', 'PHP_CodeSniffer_File' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/File.php', 'PHP_CodeSniffer_Fixer' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Fixer.php', 'PHP_CodeSniffer_Report' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Report.php', 'PHP_CodeSniffer_Reporting' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Reporting.php', 'PHP_CodeSniffer_Reports_Cbf' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Reports/Cbf.php', 'PHP_CodeSniffer_Reports_Checkstyle' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Reports/Checkstyle.php', 'PHP_CodeSniffer_Reports_Csv' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Reports/Csv.php', 'PHP_CodeSniffer_Reports_Diff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Reports/Diff.php', 'PHP_CodeSniffer_Reports_Emacs' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Reports/Emacs.php', 'PHP_CodeSniffer_Reports_Full' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Reports/Full.php', 'PHP_CodeSniffer_Reports_Gitblame' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Reports/Gitblame.php', 'PHP_CodeSniffer_Reports_Hgblame' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Reports/Hgblame.php', 'PHP_CodeSniffer_Reports_Info' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Reports/Info.php', 'PHP_CodeSniffer_Reports_Json' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Reports/Json.php', 'PHP_CodeSniffer_Reports_Junit' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Reports/Junit.php', 'PHP_CodeSniffer_Reports_Notifysend' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Reports/Notifysend.php', 'PHP_CodeSniffer_Reports_Source' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Reports/Source.php', 'PHP_CodeSniffer_Reports_Summary' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Reports/Summary.php', 'PHP_CodeSniffer_Reports_Svnblame' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Reports/Svnblame.php', 'PHP_CodeSniffer_Reports_VersionControl' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Reports/VersionControl.php', 'PHP_CodeSniffer_Reports_Xml' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Reports/Xml.php', 'PHP_CodeSniffer_Sniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Sniff.php', 'PHP_CodeSniffer_Standards_AbstractPatternSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/AbstractPatternSniff.php', 'PHP_CodeSniffer_Standards_AbstractScopeSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/AbstractScopeSniff.php', 'PHP_CodeSniffer_Standards_AbstractVariableSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/AbstractVariableSniff.php', 'PHP_CodeSniffer_Standards_IncorrectPatternException' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/IncorrectPatternException.php', 'PHP_CodeSniffer_Tokenizers_CSS' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Tokenizers/CSS.php', 'PHP_CodeSniffer_Tokenizers_Comment' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Tokenizers/Comment.php', 'PHP_CodeSniffer_Tokenizers_JS' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Tokenizers/JS.php', 'PHP_CodeSniffer_Tokenizers_PHP' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Tokenizers/PHP.php', 'PHP_CodeSniffer_Tokens' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Tokens.php', 'PHP_Timer' => __DIR__ . '/..' . '/phpunit/php-timer/src/Timer.php', 'PHP_Token' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_TokenWithScope' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_TokenWithScopeAndVisibility' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_ABSTRACT' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_AMPERSAND' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_AND_EQUAL' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_ARRAY' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_ARRAY_CAST' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_AS' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_ASYNC' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_AT' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_AWAIT' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_BACKTICK' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_BAD_CHARACTER' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_BOOLEAN_AND' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_BOOLEAN_OR' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_BOOL_CAST' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_BREAK' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_CALLABLE' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_CARET' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_CASE' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_CATCH' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_CHARACTER' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_CLASS' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_CLASS_C' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_CLASS_NAME_CONSTANT' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_CLONE' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_CLOSE_BRACKET' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_CLOSE_CURLY' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_CLOSE_SQUARE' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_CLOSE_TAG' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_COALESCE' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_COLON' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_COMMA' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_COMMENT' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_COMPILER_HALT_OFFSET' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_CONCAT_EQUAL' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_CONST' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_CONSTANT_ENCAPSED_STRING' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_CONTINUE' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_CURLY_OPEN' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_DEC' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_DECLARE' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_DEFAULT' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_DIR' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_DIV' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_DIV_EQUAL' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_DNUMBER' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_DO' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_DOC_COMMENT' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_DOLLAR' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_DOLLAR_OPEN_CURLY_BRACES' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_DOT' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_DOUBLE_ARROW' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_DOUBLE_CAST' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_DOUBLE_COLON' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_DOUBLE_QUOTES' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_ECHO' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_ELLIPSIS' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_ELSE' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_ELSEIF' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_EMPTY' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_ENCAPSED_AND_WHITESPACE' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_ENDDECLARE' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_ENDFOR' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_ENDFOREACH' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_ENDIF' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_ENDSWITCH' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_ENDWHILE' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_END_HEREDOC' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_ENUM' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_EQUAL' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_EQUALS' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_EVAL' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_EXCLAMATION_MARK' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_EXIT' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_EXTENDS' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_FILE' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_FINAL' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_FINALLY' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_FOR' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_FOREACH' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_FUNCTION' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_FUNC_C' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_GLOBAL' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_GOTO' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_GT' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_HALT_COMPILER' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_IF' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_IMPLEMENTS' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_IN' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_INC' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_INCLUDE' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_INCLUDE_ONCE' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_INLINE_HTML' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_INSTANCEOF' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_INSTEADOF' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_INTERFACE' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_INT_CAST' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_ISSET' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_IS_EQUAL' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_IS_GREATER_OR_EQUAL' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_IS_IDENTICAL' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_IS_NOT_EQUAL' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_IS_NOT_IDENTICAL' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_IS_SMALLER_OR_EQUAL' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_Includes' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_JOIN' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_LAMBDA_ARROW' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_LAMBDA_CP' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_LAMBDA_OP' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_LINE' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_LIST' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_LNUMBER' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_LOGICAL_AND' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_LOGICAL_OR' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_LOGICAL_XOR' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_LT' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_METHOD_C' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_MINUS' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_MINUS_EQUAL' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_MOD_EQUAL' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_MULT' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_MUL_EQUAL' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_NAMESPACE' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_NEW' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_NS_C' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_NS_SEPARATOR' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_NULLSAFE_OBJECT_OPERATOR' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_NUM_STRING' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_OBJECT_CAST' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_OBJECT_OPERATOR' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_ONUMBER' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_OPEN_BRACKET' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_OPEN_CURLY' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_OPEN_SQUARE' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_OPEN_TAG' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_OPEN_TAG_WITH_ECHO' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_OR_EQUAL' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_PAAMAYIM_NEKUDOTAYIM' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_PERCENT' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_PIPE' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_PLUS' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_PLUS_EQUAL' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_POW' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_POW_EQUAL' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_PRINT' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_PRIVATE' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_PROTECTED' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_PUBLIC' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_QUESTION_MARK' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_REQUIRE' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_REQUIRE_ONCE' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_RETURN' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_SEMICOLON' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_SHAPE' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_SL' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_SL_EQUAL' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_SPACESHIP' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_SR' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_SR_EQUAL' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_START_HEREDOC' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_STATIC' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_STRING' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_STRING_CAST' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_STRING_VARNAME' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_SUPER' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_SWITCH' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_Stream' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token/Stream.php', 'PHP_Token_Stream_CachingFactory' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token/Stream/CachingFactory.php', 'PHP_Token_THROW' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_TILDE' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_TRAIT' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_TRAIT_C' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_TRY' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_TYPE' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_TYPELIST_GT' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_TYPELIST_LT' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_UNSET' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_UNSET_CAST' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_USE' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_USE_FUNCTION' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_VAR' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_VARIABLE' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_WHERE' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_WHILE' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_WHITESPACE' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_XHP_ATTRIBUTE' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_XHP_CATEGORY' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_XHP_CATEGORY_LABEL' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_XHP_CHILDREN' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_XHP_LABEL' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_XHP_REQUIRED' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_XHP_TAG_GT' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_XHP_TAG_LT' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_XHP_TEXT' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_XOR_EQUAL' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_YIELD' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_YIELD_FROM' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PSR1_Sniffs_Classes_ClassDeclarationSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/PSR1/Sniffs/Classes/ClassDeclarationSniff.php', 'PSR1_Sniffs_Files_SideEffectsSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/PSR1/Sniffs/Files/SideEffectsSniff.php', 'PSR1_Sniffs_Methods_CamelCapsMethodNameSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/PSR1/Sniffs/Methods/CamelCapsMethodNameSniff.php', 'PSR2_Sniffs_Classes_ClassDeclarationSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/PSR2/Sniffs/Classes/ClassDeclarationSniff.php', 'PSR2_Sniffs_Classes_PropertyDeclarationSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/PSR2/Sniffs/Classes/PropertyDeclarationSniff.php', 'PSR2_Sniffs_ControlStructures_ControlStructureSpacingSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/PSR2/Sniffs/ControlStructures/ControlStructureSpacingSniff.php', 'PSR2_Sniffs_ControlStructures_ElseIfDeclarationSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/PSR2/Sniffs/ControlStructures/ElseIfDeclarationSniff.php', 'PSR2_Sniffs_ControlStructures_SwitchDeclarationSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/PSR2/Sniffs/ControlStructures/SwitchDeclarationSniff.php', 'PSR2_Sniffs_Files_ClosingTagSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/PSR2/Sniffs/Files/ClosingTagSniff.php', 'PSR2_Sniffs_Files_EndFileNewlineSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/PSR2/Sniffs/Files/EndFileNewlineSniff.php', 'PSR2_Sniffs_Methods_FunctionCallSignatureSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/PSR2/Sniffs/Methods/FunctionCallSignatureSniff.php', 'PSR2_Sniffs_Methods_FunctionClosingBraceSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/PSR2/Sniffs/Methods/FunctionClosingBraceSniff.php', 'PSR2_Sniffs_Methods_MethodDeclarationSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/PSR2/Sniffs/Methods/MethodDeclarationSniff.php', 'PSR2_Sniffs_Namespaces_NamespaceDeclarationSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/PSR2/Sniffs/Namespaces/NamespaceDeclarationSniff.php', 'PSR2_Sniffs_Namespaces_UseDeclarationSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/PSR2/Sniffs/Namespaces/UseDeclarationSniff.php', 'SebastianBergmann\\CodeCoverage\\CodeCoverage' => __DIR__ . '/..' . '/phpunit/php-code-coverage/src/CodeCoverage.php', 'SebastianBergmann\\CodeCoverage\\CoveredCodeNotExecutedException' => __DIR__ . '/..' . '/phpunit/php-code-coverage/src/Exception/CoveredCodeNotExecutedException.php', 'SebastianBergmann\\CodeCoverage\\Driver\\Driver' => __DIR__ . '/..' . '/phpunit/php-code-coverage/src/Driver/Driver.php', 'SebastianBergmann\\CodeCoverage\\Driver\\HHVM' => __DIR__ . '/..' . '/phpunit/php-code-coverage/src/Driver/HHVM.php', 'SebastianBergmann\\CodeCoverage\\Driver\\PHPDBG' => __DIR__ . '/..' . '/phpunit/php-code-coverage/src/Driver/PHPDBG.php', 'SebastianBergmann\\CodeCoverage\\Driver\\Xdebug' => __DIR__ . '/..' . '/phpunit/php-code-coverage/src/Driver/Xdebug.php', 'SebastianBergmann\\CodeCoverage\\Exception' => __DIR__ . '/..' . '/phpunit/php-code-coverage/src/Exception/Exception.php', 'SebastianBergmann\\CodeCoverage\\Filter' => __DIR__ . '/..' . '/phpunit/php-code-coverage/src/Filter.php', 'SebastianBergmann\\CodeCoverage\\InvalidArgumentException' => __DIR__ . '/..' . '/phpunit/php-code-coverage/src/Exception/InvalidArgumentException.php', 'SebastianBergmann\\CodeCoverage\\MissingCoversAnnotationException' => __DIR__ . '/..' . '/phpunit/php-code-coverage/src/Exception/MissingCoversAnnotationException.php', 'SebastianBergmann\\CodeCoverage\\Node\\AbstractNode' => __DIR__ . '/..' . '/phpunit/php-code-coverage/src/Node/AbstractNode.php', 'SebastianBergmann\\CodeCoverage\\Node\\Builder' => __DIR__ . '/..' . '/phpunit/php-code-coverage/src/Node/Builder.php', 'SebastianBergmann\\CodeCoverage\\Node\\Directory' => __DIR__ . '/..' . '/phpunit/php-code-coverage/src/Node/Directory.php', 'SebastianBergmann\\CodeCoverage\\Node\\File' => __DIR__ . '/..' . '/phpunit/php-code-coverage/src/Node/File.php', 'SebastianBergmann\\CodeCoverage\\Node\\Iterator' => __DIR__ . '/..' . '/phpunit/php-code-coverage/src/Node/Iterator.php', 'SebastianBergmann\\CodeCoverage\\Report\\Clover' => __DIR__ . '/..' . '/phpunit/php-code-coverage/src/Report/Clover.php', 'SebastianBergmann\\CodeCoverage\\Report\\Crap4j' => __DIR__ . '/..' . '/phpunit/php-code-coverage/src/Report/Crap4j.php', 'SebastianBergmann\\CodeCoverage\\Report\\Html\\Dashboard' => __DIR__ . '/..' . '/phpunit/php-code-coverage/src/Report/Html/Renderer/Dashboard.php', 'SebastianBergmann\\CodeCoverage\\Report\\Html\\Directory' => __DIR__ . '/..' . '/phpunit/php-code-coverage/src/Report/Html/Renderer/Directory.php', 'SebastianBergmann\\CodeCoverage\\Report\\Html\\Facade' => __DIR__ . '/..' . '/phpunit/php-code-coverage/src/Report/Html/Facade.php', 'SebastianBergmann\\CodeCoverage\\Report\\Html\\File' => __DIR__ . '/..' . '/phpunit/php-code-coverage/src/Report/Html/Renderer/File.php', 'SebastianBergmann\\CodeCoverage\\Report\\Html\\Renderer' => __DIR__ . '/..' . '/phpunit/php-code-coverage/src/Report/Html/Renderer.php', 'SebastianBergmann\\CodeCoverage\\Report\\PHP' => __DIR__ . '/..' . '/phpunit/php-code-coverage/src/Report/PHP.php', 'SebastianBergmann\\CodeCoverage\\Report\\Text' => __DIR__ . '/..' . '/phpunit/php-code-coverage/src/Report/Text.php', 'SebastianBergmann\\CodeCoverage\\Report\\Xml\\Coverage' => __DIR__ . '/..' . '/phpunit/php-code-coverage/src/Report/Xml/Coverage.php', 'SebastianBergmann\\CodeCoverage\\Report\\Xml\\Directory' => __DIR__ . '/..' . '/phpunit/php-code-coverage/src/Report/Xml/Directory.php', 'SebastianBergmann\\CodeCoverage\\Report\\Xml\\Facade' => __DIR__ . '/..' . '/phpunit/php-code-coverage/src/Report/Xml/Facade.php', 'SebastianBergmann\\CodeCoverage\\Report\\Xml\\File' => __DIR__ . '/..' . '/phpunit/php-code-coverage/src/Report/Xml/File.php', 'SebastianBergmann\\CodeCoverage\\Report\\Xml\\Method' => __DIR__ . '/..' . '/phpunit/php-code-coverage/src/Report/Xml/Method.php', 'SebastianBergmann\\CodeCoverage\\Report\\Xml\\Node' => __DIR__ . '/..' . '/phpunit/php-code-coverage/src/Report/Xml/Node.php', 'SebastianBergmann\\CodeCoverage\\Report\\Xml\\Project' => __DIR__ . '/..' . '/phpunit/php-code-coverage/src/Report/Xml/Project.php', 'SebastianBergmann\\CodeCoverage\\Report\\Xml\\Report' => __DIR__ . '/..' . '/phpunit/php-code-coverage/src/Report/Xml/Report.php', 'SebastianBergmann\\CodeCoverage\\Report\\Xml\\Tests' => __DIR__ . '/..' . '/phpunit/php-code-coverage/src/Report/Xml/Tests.php', 'SebastianBergmann\\CodeCoverage\\Report\\Xml\\Totals' => __DIR__ . '/..' . '/phpunit/php-code-coverage/src/Report/Xml/Totals.php', 'SebastianBergmann\\CodeCoverage\\Report\\Xml\\Unit' => __DIR__ . '/..' . '/phpunit/php-code-coverage/src/Report/Xml/Unit.php', 'SebastianBergmann\\CodeCoverage\\RuntimeException' => __DIR__ . '/..' . '/phpunit/php-code-coverage/src/Exception/RuntimeException.php', 'SebastianBergmann\\CodeCoverage\\UnintentionallyCoveredCodeException' => __DIR__ . '/..' . '/phpunit/php-code-coverage/src/Exception/UnintentionallyCoveredCodeException.php', 'SebastianBergmann\\CodeCoverage\\Util' => __DIR__ . '/..' . '/phpunit/php-code-coverage/src/Util.php', 'SebastianBergmann\\CodeUnitReverseLookup\\Wizard' => __DIR__ . '/..' . '/sebastian/code-unit-reverse-lookup/src/Wizard.php', 'SebastianBergmann\\Comparator\\ArrayComparator' => __DIR__ . '/..' . '/sebastian/comparator/src/ArrayComparator.php', 'SebastianBergmann\\Comparator\\Comparator' => __DIR__ . '/..' . '/sebastian/comparator/src/Comparator.php', 'SebastianBergmann\\Comparator\\ComparisonFailure' => __DIR__ . '/..' . '/sebastian/comparator/src/ComparisonFailure.php', 'SebastianBergmann\\Comparator\\DOMNodeComparator' => __DIR__ . '/..' . '/sebastian/comparator/src/DOMNodeComparator.php', 'SebastianBergmann\\Comparator\\DateTimeComparator' => __DIR__ . '/..' . '/sebastian/comparator/src/DateTimeComparator.php', 'SebastianBergmann\\Comparator\\DoubleComparator' => __DIR__ . '/..' . '/sebastian/comparator/src/DoubleComparator.php', 'SebastianBergmann\\Comparator\\ExceptionComparator' => __DIR__ . '/..' . '/sebastian/comparator/src/ExceptionComparator.php', 'SebastianBergmann\\Comparator\\Factory' => __DIR__ . '/..' . '/sebastian/comparator/src/Factory.php', 'SebastianBergmann\\Comparator\\MockObjectComparator' => __DIR__ . '/..' . '/sebastian/comparator/src/MockObjectComparator.php', 'SebastianBergmann\\Comparator\\NumericComparator' => __DIR__ . '/..' . '/sebastian/comparator/src/NumericComparator.php', 'SebastianBergmann\\Comparator\\ObjectComparator' => __DIR__ . '/..' . '/sebastian/comparator/src/ObjectComparator.php', 'SebastianBergmann\\Comparator\\ResourceComparator' => __DIR__ . '/..' . '/sebastian/comparator/src/ResourceComparator.php', 'SebastianBergmann\\Comparator\\ScalarComparator' => __DIR__ . '/..' . '/sebastian/comparator/src/ScalarComparator.php', 'SebastianBergmann\\Comparator\\SplObjectStorageComparator' => __DIR__ . '/..' . '/sebastian/comparator/src/SplObjectStorageComparator.php', 'SebastianBergmann\\Comparator\\TypeComparator' => __DIR__ . '/..' . '/sebastian/comparator/src/TypeComparator.php', 'SebastianBergmann\\Diff\\Chunk' => __DIR__ . '/..' . '/sebastian/diff/src/Chunk.php', 'SebastianBergmann\\Diff\\Diff' => __DIR__ . '/..' . '/sebastian/diff/src/Diff.php', 'SebastianBergmann\\Diff\\Differ' => __DIR__ . '/..' . '/sebastian/diff/src/Differ.php', 'SebastianBergmann\\Diff\\LCS\\LongestCommonSubsequence' => __DIR__ . '/..' . '/sebastian/diff/src/LCS/LongestCommonSubsequence.php', 'SebastianBergmann\\Diff\\LCS\\MemoryEfficientImplementation' => __DIR__ . '/..' . '/sebastian/diff/src/LCS/MemoryEfficientLongestCommonSubsequenceImplementation.php', 'SebastianBergmann\\Diff\\LCS\\TimeEfficientImplementation' => __DIR__ . '/..' . '/sebastian/diff/src/LCS/TimeEfficientLongestCommonSubsequenceImplementation.php', 'SebastianBergmann\\Diff\\Line' => __DIR__ . '/..' . '/sebastian/diff/src/Line.php', 'SebastianBergmann\\Diff\\Parser' => __DIR__ . '/..' . '/sebastian/diff/src/Parser.php', 'SebastianBergmann\\Environment\\Console' => __DIR__ . '/..' . '/sebastian/environment/src/Console.php', 'SebastianBergmann\\Environment\\Runtime' => __DIR__ . '/..' . '/sebastian/environment/src/Runtime.php', 'SebastianBergmann\\Exporter\\Exporter' => __DIR__ . '/..' . '/sebastian/exporter/src/Exporter.php', 'SebastianBergmann\\GlobalState\\Blacklist' => __DIR__ . '/..' . '/sebastian/global-state/src/Blacklist.php', 'SebastianBergmann\\GlobalState\\CodeExporter' => __DIR__ . '/..' . '/sebastian/global-state/src/CodeExporter.php', 'SebastianBergmann\\GlobalState\\Exception' => __DIR__ . '/..' . '/sebastian/global-state/src/Exception.php', 'SebastianBergmann\\GlobalState\\Restorer' => __DIR__ . '/..' . '/sebastian/global-state/src/Restorer.php', 'SebastianBergmann\\GlobalState\\RuntimeException' => __DIR__ . '/..' . '/sebastian/global-state/src/RuntimeException.php', 'SebastianBergmann\\GlobalState\\Snapshot' => __DIR__ . '/..' . '/sebastian/global-state/src/Snapshot.php', 'SebastianBergmann\\ObjectEnumerator\\Enumerator' => __DIR__ . '/..' . '/sebastian/object-enumerator/src/Enumerator.php', 'SebastianBergmann\\ObjectEnumerator\\Exception' => __DIR__ . '/..' . '/sebastian/object-enumerator/src/Exception.php', 'SebastianBergmann\\ObjectEnumerator\\InvalidArgumentException' => __DIR__ . '/..' . '/sebastian/object-enumerator/src/InvalidArgumentException.php', 'SebastianBergmann\\RecursionContext\\Context' => __DIR__ . '/..' . '/sebastian/recursion-context/src/Context.php', 'SebastianBergmann\\RecursionContext\\Exception' => __DIR__ . '/..' . '/sebastian/recursion-context/src/Exception.php', 'SebastianBergmann\\RecursionContext\\InvalidArgumentException' => __DIR__ . '/..' . '/sebastian/recursion-context/src/InvalidArgumentException.php', 'SebastianBergmann\\ResourceOperations\\ResourceOperations' => __DIR__ . '/..' . '/sebastian/resource-operations/src/ResourceOperations.php', 'SebastianBergmann\\Version' => __DIR__ . '/..' . '/sebastian/version/src/Version.php', 'Squiz_Sniffs_Arrays_ArrayBracketSpacingSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/Arrays/ArrayBracketSpacingSniff.php', 'Squiz_Sniffs_Arrays_ArrayDeclarationSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/Arrays/ArrayDeclarationSniff.php', 'Squiz_Sniffs_CSS_ClassDefinitionClosingBraceSpaceSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/CSS/ClassDefinitionClosingBraceSpaceSniff.php', 'Squiz_Sniffs_CSS_ClassDefinitionNameSpacingSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/CSS/ClassDefinitionNameSpacingSniff.php', 'Squiz_Sniffs_CSS_ClassDefinitionOpeningBraceSpaceSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/CSS/ClassDefinitionOpeningBraceSpaceSniff.php', 'Squiz_Sniffs_CSS_ColonSpacingSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/CSS/ColonSpacingSniff.php', 'Squiz_Sniffs_CSS_ColourDefinitionSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/CSS/ColourDefinitionSniff.php', 'Squiz_Sniffs_CSS_DisallowMultipleStyleDefinitionsSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/CSS/DisallowMultipleStyleDefinitionsSniff.php', 'Squiz_Sniffs_CSS_DuplicateClassDefinitionSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/CSS/DuplicateClassDefinitionSniff.php', 'Squiz_Sniffs_CSS_DuplicateStyleDefinitionSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/CSS/DuplicateStyleDefinitionSniff.php', 'Squiz_Sniffs_CSS_EmptyClassDefinitionSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/CSS/EmptyClassDefinitionSniff.php', 'Squiz_Sniffs_CSS_EmptyStyleDefinitionSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/CSS/EmptyStyleDefinitionSniff.php', 'Squiz_Sniffs_CSS_ForbiddenStylesSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/CSS/ForbiddenStylesSniff.php', 'Squiz_Sniffs_CSS_IndentationSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/CSS/IndentationSniff.php', 'Squiz_Sniffs_CSS_LowercaseStyleDefinitionSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/CSS/LowercaseStyleDefinitionSniff.php', 'Squiz_Sniffs_CSS_MissingColonSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/CSS/MissingColonSniff.php', 'Squiz_Sniffs_CSS_NamedColoursSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/CSS/NamedColoursSniff.php', 'Squiz_Sniffs_CSS_OpacitySniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/CSS/OpacitySniff.php', 'Squiz_Sniffs_CSS_SemicolonSpacingSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/CSS/SemicolonSpacingSniff.php', 'Squiz_Sniffs_CSS_ShorthandSizeSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/CSS/ShorthandSizeSniff.php', 'Squiz_Sniffs_Classes_ClassDeclarationSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/Classes/ClassDeclarationSniff.php', 'Squiz_Sniffs_Classes_ClassFileNameSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/Classes/ClassFileNameSniff.php', 'Squiz_Sniffs_Classes_DuplicatePropertySniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/Classes/DuplicatePropertySniff.php', 'Squiz_Sniffs_Classes_LowercaseClassKeywordsSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/Classes/LowercaseClassKeywordsSniff.php', 'Squiz_Sniffs_Classes_SelfMemberReferenceSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/Classes/SelfMemberReferenceSniff.php', 'Squiz_Sniffs_Classes_ValidClassNameSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/Classes/ValidClassNameSniff.php', 'Squiz_Sniffs_Commenting_BlockCommentSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/Commenting/BlockCommentSniff.php', 'Squiz_Sniffs_Commenting_ClassCommentSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/Commenting/ClassCommentSniff.php', 'Squiz_Sniffs_Commenting_ClosingDeclarationCommentSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/Commenting/ClosingDeclarationCommentSniff.php', 'Squiz_Sniffs_Commenting_DocCommentAlignmentSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/Commenting/DocCommentAlignmentSniff.php', 'Squiz_Sniffs_Commenting_EmptyCatchCommentSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/Commenting/EmptyCatchCommentSniff.php', 'Squiz_Sniffs_Commenting_FileCommentSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/Commenting/FileCommentSniff.php', 'Squiz_Sniffs_Commenting_FunctionCommentSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/Commenting/FunctionCommentSniff.php', 'Squiz_Sniffs_Commenting_FunctionCommentThrowTagSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/Commenting/FunctionCommentThrowTagSniff.php', 'Squiz_Sniffs_Commenting_InlineCommentSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/Commenting/InlineCommentSniff.php', 'Squiz_Sniffs_Commenting_LongConditionClosingCommentSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/Commenting/LongConditionClosingCommentSniff.php', 'Squiz_Sniffs_Commenting_PostStatementCommentSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/Commenting/PostStatementCommentSniff.php', 'Squiz_Sniffs_Commenting_VariableCommentSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/Commenting/VariableCommentSniff.php', 'Squiz_Sniffs_ControlStructures_ControlSignatureSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/ControlStructures/ControlSignatureSniff.php', 'Squiz_Sniffs_ControlStructures_ElseIfDeclarationSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/ControlStructures/ElseIfDeclarationSniff.php', 'Squiz_Sniffs_ControlStructures_ForEachLoopDeclarationSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/ControlStructures/ForEachLoopDeclarationSniff.php', 'Squiz_Sniffs_ControlStructures_ForLoopDeclarationSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/ControlStructures/ForLoopDeclarationSniff.php', 'Squiz_Sniffs_ControlStructures_InlineIfDeclarationSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/ControlStructures/InlineIfDeclarationSniff.php', 'Squiz_Sniffs_ControlStructures_LowercaseDeclarationSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/ControlStructures/LowercaseDeclarationSniff.php', 'Squiz_Sniffs_ControlStructures_SwitchDeclarationSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/ControlStructures/SwitchDeclarationSniff.php', 'Squiz_Sniffs_Debug_JSLintSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/Debug/JSLintSniff.php', 'Squiz_Sniffs_Debug_JavaScriptLintSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/Debug/JavaScriptLintSniff.php', 'Squiz_Sniffs_Files_FileExtensionSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/Files/FileExtensionSniff.php', 'Squiz_Sniffs_Formatting_OperatorBracketSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/Formatting/OperatorBracketSniff.php', 'Squiz_Sniffs_Functions_FunctionDeclarationArgumentSpacingSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/Functions/FunctionDeclarationArgumentSpacingSniff.php', 'Squiz_Sniffs_Functions_FunctionDeclarationSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/Functions/FunctionDeclarationSniff.php', 'Squiz_Sniffs_Functions_FunctionDuplicateArgumentSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/Functions/FunctionDuplicateArgumentSniff.php', 'Squiz_Sniffs_Functions_GlobalFunctionSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/Functions/GlobalFunctionSniff.php', 'Squiz_Sniffs_Functions_LowercaseFunctionKeywordsSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/Functions/LowercaseFunctionKeywordsSniff.php', 'Squiz_Sniffs_Functions_MultiLineFunctionDeclarationSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/Functions/MultiLineFunctionDeclarationSniff.php', 'Squiz_Sniffs_NamingConventions_ValidFunctionNameSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/NamingConventions/ValidFunctionNameSniff.php', 'Squiz_Sniffs_NamingConventions_ValidVariableNameSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/NamingConventions/ValidVariableNameSniff.php', 'Squiz_Sniffs_Objects_DisallowObjectStringIndexSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/Objects/DisallowObjectStringIndexSniff.php', 'Squiz_Sniffs_Objects_ObjectInstantiationSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/Objects/ObjectInstantiationSniff.php', 'Squiz_Sniffs_Objects_ObjectMemberCommaSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/Objects/ObjectMemberCommaSniff.php', 'Squiz_Sniffs_Operators_ComparisonOperatorUsageSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/Operators/ComparisonOperatorUsageSniff.php', 'Squiz_Sniffs_Operators_IncrementDecrementUsageSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/Operators/IncrementDecrementUsageSniff.php', 'Squiz_Sniffs_Operators_ValidLogicalOperatorsSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/Operators/ValidLogicalOperatorsSniff.php', 'Squiz_Sniffs_PHP_CommentedOutCodeSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/PHP/CommentedOutCodeSniff.php', 'Squiz_Sniffs_PHP_DisallowBooleanStatementSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/PHP/DisallowBooleanStatementSniff.php', 'Squiz_Sniffs_PHP_DisallowComparisonAssignmentSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/PHP/DisallowComparisonAssignmentSniff.php', 'Squiz_Sniffs_PHP_DisallowInlineIfSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/PHP/DisallowInlineIfSniff.php', 'Squiz_Sniffs_PHP_DisallowMultipleAssignmentsSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/PHP/DisallowMultipleAssignmentsSniff.php', 'Squiz_Sniffs_PHP_DisallowObEndFlushSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/PHP/DisallowObEndFlushSniff.php', 'Squiz_Sniffs_PHP_DisallowSizeFunctionsInLoopsSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/PHP/DisallowSizeFunctionsInLoopsSniff.php', 'Squiz_Sniffs_PHP_DiscouragedFunctionsSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/PHP/DiscouragedFunctionsSniff.php', 'Squiz_Sniffs_PHP_EmbeddedPhpSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/PHP/EmbeddedPhpSniff.php', 'Squiz_Sniffs_PHP_EvalSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/PHP/EvalSniff.php', 'Squiz_Sniffs_PHP_ForbiddenFunctionsSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/PHP/ForbiddenFunctionsSniff.php', 'Squiz_Sniffs_PHP_GlobalKeywordSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/PHP/GlobalKeywordSniff.php', 'Squiz_Sniffs_PHP_HeredocSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/PHP/HeredocSniff.php', 'Squiz_Sniffs_PHP_InnerFunctionsSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/PHP/InnerFunctionsSniff.php', 'Squiz_Sniffs_PHP_LowercasePHPFunctionsSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/PHP/LowercasePHPFunctionsSniff.php', 'Squiz_Sniffs_PHP_NonExecutableCodeSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/PHP/NonExecutableCodeSniff.php', 'Squiz_Sniffs_Scope_MemberVarScopeSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/Scope/MemberVarScopeSniff.php', 'Squiz_Sniffs_Scope_MethodScopeSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/Scope/MethodScopeSniff.php', 'Squiz_Sniffs_Scope_StaticThisUsageSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/Scope/StaticThisUsageSniff.php', 'Squiz_Sniffs_Strings_ConcatenationSpacingSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/Strings/ConcatenationSpacingSniff.php', 'Squiz_Sniffs_Strings_DoubleQuoteUsageSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/Strings/DoubleQuoteUsageSniff.php', 'Squiz_Sniffs_Strings_EchoedStringsSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/Strings/EchoedStringsSniff.php', 'Squiz_Sniffs_WhiteSpace_CastSpacingSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/WhiteSpace/CastSpacingSniff.php', 'Squiz_Sniffs_WhiteSpace_ControlStructureSpacingSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/WhiteSpace/ControlStructureSpacingSniff.php', 'Squiz_Sniffs_WhiteSpace_FunctionClosingBraceSpaceSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/WhiteSpace/FunctionClosingBraceSpaceSniff.php', 'Squiz_Sniffs_WhiteSpace_FunctionOpeningBraceSpaceSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/WhiteSpace/FunctionOpeningBraceSpaceSniff.php', 'Squiz_Sniffs_WhiteSpace_FunctionSpacingSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/WhiteSpace/FunctionSpacingSniff.php', 'Squiz_Sniffs_WhiteSpace_LanguageConstructSpacingSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/WhiteSpace/LanguageConstructSpacingSniff.php', 'Squiz_Sniffs_WhiteSpace_LogicalOperatorSpacingSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/WhiteSpace/LogicalOperatorSpacingSniff.php', 'Squiz_Sniffs_WhiteSpace_MemberVarSpacingSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/WhiteSpace/MemberVarSpacingSniff.php', 'Squiz_Sniffs_WhiteSpace_ObjectOperatorSpacingSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/WhiteSpace/ObjectOperatorSpacingSniff.php', 'Squiz_Sniffs_WhiteSpace_OperatorSpacingSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/WhiteSpace/OperatorSpacingSniff.php', 'Squiz_Sniffs_WhiteSpace_PropertyLabelSpacingSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/WhiteSpace/PropertyLabelSpacingSniff.php', 'Squiz_Sniffs_WhiteSpace_ScopeClosingBraceSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/WhiteSpace/ScopeClosingBraceSniff.php', 'Squiz_Sniffs_WhiteSpace_ScopeKeywordSpacingSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/WhiteSpace/ScopeKeywordSpacingSniff.php', 'Squiz_Sniffs_WhiteSpace_SemicolonSpacingSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/WhiteSpace/SemicolonSpacingSniff.php', 'Squiz_Sniffs_WhiteSpace_SuperfluousWhitespaceSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/WhiteSpace/SuperfluousWhitespaceSniff.php', 'Text_Template' => __DIR__ . '/..' . '/phpunit/php-text-template/src/Template.php', 'Zend_Sniffs_Debug_CodeAnalyzerSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Zend/Sniffs/Debug/CodeAnalyzerSniff.php', 'Zend_Sniffs_Files_ClosingTagSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Zend/Sniffs/Files/ClosingTagSniff.php', 'Zend_Sniffs_NamingConventions_ValidVariableNameSniff' => __DIR__ . '/..' . '/squizlabs/php_codesniffer/CodeSniffer/Standards/Zend/Sniffs/NamingConventions/ValidVariableNameSniff.php', ); public static function getInitializer(ClassLoader $loader) { return \Closure::bind(function () use ($loader) { $loader->prefixLengthsPsr4 = ComposerStaticInite0bcc21ff65691a5e7c90d59ffab2d23::$prefixLengthsPsr4; $loader->prefixDirsPsr4 = ComposerStaticInite0bcc21ff65691a5e7c90d59ffab2d23::$prefixDirsPsr4; $loader->prefixesPsr0 = ComposerStaticInite0bcc21ff65691a5e7c90d59ffab2d23::$prefixesPsr0; $loader->classMap = ComposerStaticInite0bcc21ff65691a5e7c90d59ffab2d23::$classMap; }, null, ClassLoader::class); } } prefixesPsr0)) { return call_user_func_array('array_merge', $this->prefixesPsr0); } return array(); } public function getPrefixesPsr4() { return $this->prefixDirsPsr4; } public function getFallbackDirs() { return $this->fallbackDirsPsr0; } public function getFallbackDirsPsr4() { return $this->fallbackDirsPsr4; } public function getClassMap() { return $this->classMap; } public function addClassMap(array $classMap) { if ($this->classMap) { $this->classMap = array_merge($this->classMap, $classMap); } else { $this->classMap = $classMap; } } public function add($prefix, $paths, $prepend = false) { if (!$prefix) { if ($prepend) { $this->fallbackDirsPsr0 = array_merge( (array) $paths, $this->fallbackDirsPsr0 ); } else { $this->fallbackDirsPsr0 = array_merge( $this->fallbackDirsPsr0, (array) $paths ); } return; } $first = $prefix[0]; if (!isset($this->prefixesPsr0[$first][$prefix])) { $this->prefixesPsr0[$first][$prefix] = (array) $paths; return; } if ($prepend) { $this->prefixesPsr0[$first][$prefix] = array_merge( (array) $paths, $this->prefixesPsr0[$first][$prefix] ); } else { $this->prefixesPsr0[$first][$prefix] = array_merge( $this->prefixesPsr0[$first][$prefix], (array) $paths ); } } public function addPsr4($prefix, $paths, $prepend = false) { if (!$prefix) { if ($prepend) { $this->fallbackDirsPsr4 = array_merge( (array) $paths, $this->fallbackDirsPsr4 ); } else { $this->fallbackDirsPsr4 = array_merge( $this->fallbackDirsPsr4, (array) $paths ); } } elseif (!isset($this->prefixDirsPsr4[$prefix])) { $length = strlen($prefix); if ('\\' !== $prefix[$length - 1]) { throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); } $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; $this->prefixDirsPsr4[$prefix] = (array) $paths; } elseif ($prepend) { $this->prefixDirsPsr4[$prefix] = array_merge( (array) $paths, $this->prefixDirsPsr4[$prefix] ); } else { $this->prefixDirsPsr4[$prefix] = array_merge( $this->prefixDirsPsr4[$prefix], (array) $paths ); } } public function set($prefix, $paths) { if (!$prefix) { $this->fallbackDirsPsr0 = (array) $paths; } else { $this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths; } } public function setPsr4($prefix, $paths) { if (!$prefix) { $this->fallbackDirsPsr4 = (array) $paths; } else { $length = strlen($prefix); if ('\\' !== $prefix[$length - 1]) { throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); } $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; $this->prefixDirsPsr4[$prefix] = (array) $paths; } } public function setUseIncludePath($useIncludePath) { $this->useIncludePath = $useIncludePath; } public function getUseIncludePath() { return $this->useIncludePath; } public function setClassMapAuthoritative($classMapAuthoritative) { $this->classMapAuthoritative = $classMapAuthoritative; } public function isClassMapAuthoritative() { return $this->classMapAuthoritative; } public function setApcuPrefix($apcuPrefix) { $this->apcuPrefix = function_exists('apcu_fetch') && ini_get('apc.enabled') ? $apcuPrefix : null; } public function getApcuPrefix() { return $this->apcuPrefix; } public function register($prepend = false) { spl_autoload_register(array($this, 'loadClass'), true, $prepend); } public function unregister() { spl_autoload_unregister(array($this, 'loadClass')); } public function loadClass($class) { if ($file = $this->findFile($class)) { includeFile($file); return true; } } public function findFile($class) { if (isset($this->classMap[$class])) { return $this->classMap[$class]; } if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) { return false; } if (null !== $this->apcuPrefix) { $file = apcu_fetch($this->apcuPrefix.$class, $hit); if ($hit) { return $file; } } $file = $this->findFileWithExtension($class, '.php'); if (false === $file && defined('HHVM_VERSION')) { $file = $this->findFileWithExtension($class, '.hh'); } if (null !== $this->apcuPrefix) { apcu_add($this->apcuPrefix.$class, $file); } if (false === $file) { $this->missingClasses[$class] = true; } return $file; } private function findFileWithExtension($class, $ext) { $logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext; $first = $class[0]; if (isset($this->prefixLengthsPsr4[$first])) { $subPath = $class; while (false !== $lastPos = strrpos($subPath, '\\')) { $subPath = substr($subPath, 0, $lastPos); $search = $subPath.'\\'; if (isset($this->prefixDirsPsr4[$search])) { $pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1); foreach ($this->prefixDirsPsr4[$search] as $dir) { if (file_exists($file = $dir . $pathEnd)) { return $file; } } } } } foreach ($this->fallbackDirsPsr4 as $dir) { if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) { return $file; } } if (false !== $pos = strrpos($class, '\\')) { $logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1) . strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR); } else { $logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext; } if (isset($this->prefixesPsr0[$first])) { foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) { if (0 === strpos($class, $prefix)) { foreach ($dirs as $dir) { if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { return $file; } } } } } foreach ($this->fallbackDirsPsr0 as $dir) { if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { return $file; } } if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) { return $file; } return false; } } function includeFile($file) { include $file; } = 50400 && trait_exists($className)) { return new self(sprintf('The provided type "%s" is a trait, and can not be instantiated', $className)); } return new self(sprintf('The provided class "%s" does not exist', $className)); } public static function fromAbstractClass(ReflectionClass $reflectionClass) { return new self(sprintf( 'The provided class "%s" is abstract, and can not be instantiated', $reflectionClass->getName() )); } } getName() ), 0, $exception ); } public static function fromUncleanUnSerialization( ReflectionClass $reflectionClass, $errorString, $errorCode, $errorFile, $errorLine ) { return new self( sprintf( 'Could not produce an instance of "%s" via un-serialization, since an error was triggered ' . 'in file "%s" at line "%d"', $reflectionClass->getName(), $errorFile, $errorLine ), 0, new Exception($errorString, $errorCode) ); } } buildAndCacheFromFactory($className); } private function buildAndCacheFromFactory($className) { $factory = self::$cachedInstantiators[$className] = $this->buildFactory($className); $instance = $factory(); if ($this->isSafeToClone(new ReflectionClass($instance))) { self::$cachedCloneables[$className] = clone $instance; } return $instance; } private function buildFactory($className) { $reflectionClass = $this->getReflectionClass($className); if ($this->isInstantiableViaReflection($reflectionClass)) { return function () use ($reflectionClass) { return $reflectionClass->newInstanceWithoutConstructor(); }; } $serializedString = sprintf( '%s:%d:"%s":0:{}', $this->getSerializationFormat($reflectionClass), strlen($className), $className ); $this->checkIfUnSerializationIsSupported($reflectionClass, $serializedString); return function () use ($serializedString) { return unserialize($serializedString); }; } private function getReflectionClass($className) { if (! class_exists($className)) { throw InvalidArgumentException::fromNonExistingClass($className); } $reflection = new ReflectionClass($className); if ($reflection->isAbstract()) { throw InvalidArgumentException::fromAbstractClass($reflection); } return $reflection; } private function checkIfUnSerializationIsSupported(ReflectionClass $reflectionClass, $serializedString) { set_error_handler(function ($code, $message, $file, $line) use ($reflectionClass, & $error) { $error = UnexpectedValueException::fromUncleanUnSerialization( $reflectionClass, $message, $code, $file, $line ); }); $this->attemptInstantiationViaUnSerialization($reflectionClass, $serializedString); restore_error_handler(); if ($error) { throw $error; } } private function attemptInstantiationViaUnSerialization(ReflectionClass $reflectionClass, $serializedString) { try { unserialize($serializedString); } catch (Exception $exception) { restore_error_handler(); throw UnexpectedValueException::fromSerializationTriggeredException($reflectionClass, $exception); } } private function isInstantiableViaReflection(ReflectionClass $reflectionClass) { if (\PHP_VERSION_ID >= 50600) { return ! ($this->hasInternalAncestors($reflectionClass) && $reflectionClass->isFinal()); } return \PHP_VERSION_ID >= 50400 && ! $this->hasInternalAncestors($reflectionClass); } private function hasInternalAncestors(ReflectionClass $reflectionClass) { do { if ($reflectionClass->isInternal()) { return true; } } while ($reflectionClass = $reflectionClass->getParentClass()); return false; } private function getSerializationFormat(ReflectionClass $reflectionClass) { if ($this->isPhpVersionWithBrokenSerializationFormat() && $reflectionClass->implementsInterface('Serializable') ) { return self::SERIALIZATION_FORMAT_USE_UNSERIALIZER; } return self::SERIALIZATION_FORMAT_AVOID_UNSERIALIZER; } private function isPhpVersionWithBrokenSerializationFormat() { return PHP_VERSION_ID === 50429 || PHP_VERSION_ID === 50513; } private function isSafeToClone(ReflectionClass $reflection) { if (method_exists($reflection, 'isCloneable') && ! $reflection->isCloneable()) { return false; } return ! $reflection->hasMethod('__clone'); } } getTagName(); if ($tagName !== 'input') { throw new UnexpectedTagNameException('input', $tagName); } $this->name = $element->getAttribute('name'); if ($this->name === null) { throw new WebDriverException('The input does not have a "name" attribute.'); } $this->element = $element; } public function getOptions() { return $this->getRelatedElements(); } public function getAllSelectedOptions() { $selectedElement = []; foreach ($this->getRelatedElements() as $element) { if ($element->isSelected()) { $selectedElement[] = $element; if (!$this->isMultiple()) { return $selectedElement; } } } return $selectedElement; } public function getFirstSelectedOption() { foreach ($this->getRelatedElements() as $element) { if ($element->isSelected()) { return $element; } } throw new NoSuchElementException( sprintf('No %s are selected', 'radio' === $this->type ? 'radio buttons' : 'checkboxes') ); } public function selectByIndex($index) { $this->byIndex($index); } public function selectByValue($value) { $this->byValue($value); } public function selectByVisibleText($text) { $this->byVisibleText($text); } public function selectByVisiblePartialText($text) { $this->byVisibleText($text, true); } protected function byValue($value, $select = true) { $matched = false; foreach ($this->getRelatedElements($value) as $element) { $select ? $this->selectOption($element) : $this->deselectOption($element); if (!$this->isMultiple()) { return; } $matched = true; } if (!$matched) { throw new NoSuchElementException( sprintf('Cannot locate %s with value: %s', $this->type, $value) ); } } protected function byIndex($index, $select = true) { $elements = $this->getRelatedElements(); if (!isset($elements[$index])) { throw new NoSuchElementException(sprintf('Cannot locate %s with index: %d', $this->type, $index)); } $select ? $this->selectOption($elements[$index]) : $this->deselectOption($elements[$index]); } protected function byVisibleText($text, $partial = false, $select = true) { foreach ($this->getRelatedElements() as $element) { $normalizeFilter = sprintf( $partial ? 'contains(normalize-space(.), %s)' : 'normalize-space(.) = %s', XPathEscaper::escapeQuotes($text) ); $xpath = 'ancestor::label'; $xpathNormalize = sprintf('%s[%s]', $xpath, $normalizeFilter); $id = $element->getAttribute('id'); if ($id !== null) { $idFilter = sprintf('@for = %s', XPathEscaper::escapeQuotes($id)); $xpath .= sprintf(' | //label[%s]', $idFilter); $xpathNormalize .= sprintf(' | //label[%s and %s]', $idFilter, $normalizeFilter); } try { $element->findElement(WebDriverBy::xpath($xpathNormalize)); } catch (NoSuchElementException $e) { if ($partial) { continue; } try { if ($text !== $element->findElement(WebDriverBy::xpath($xpath))->getText()) { continue; } } catch (NoSuchElementException $e) { continue; } } $select ? $this->selectOption($element) : $this->deselectOption($element); if (!$this->isMultiple()) { return; } } } protected function getRelatedElements($value = null) { $valueSelector = $value ? sprintf(' and @value = %s', XPathEscaper::escapeQuotes($value)) : ''; $formId = $this->element->getAttribute('form'); if ($formId === null) { $form = $this->element->findElement(WebDriverBy::xpath('ancestor::form')); $formId = $form->getAttribute('id'); if ($formId === '') { return $form->findElements(WebDriverBy::xpath( sprintf('.//input[@name = %s%s]', XPathEscaper::escapeQuotes($this->name), $valueSelector) )); } } return $this->element->findElements( WebDriverBy::xpath(sprintf( '//form[@id = %1$s]//input[@name = %2$s%3$s' . ' and ((boolean(@form) = true() and @form = %1$s) or boolean(@form) = false())]' . ' | //input[@form = %1$s and @name = %2$s%3$s]', XPathEscaper::escapeQuotes($formId), XPathEscaper::escapeQuotes($this->name), $valueSelector )) ); } protected function selectOption(WebDriverElement $element) { if (!$element->isSelected()) { $element->click(); } } protected function deselectOption(WebDriverElement $element) { if ($element->isSelected()) { $element->click(); } } } startSession($desired_capabilities); return $driver; } public function startSession(DesiredCapabilities $desired_capabilities) { $command = new WebDriverCommand( null, DriverCommand::NEW_SESSION, [ 'desiredCapabilities' => $desired_capabilities->toArray(), ] ); $response = $this->executor->execute($command); $this->sessionID = $response->getSessionID(); } public static function create( $selenium_server_url = 'http://localhost:4444/wd/hub', $desired_capabilities = null, $connection_timeout_in_ms = null, $request_timeout_in_ms = null, $http_proxy = null, $http_proxy_port = null, DesiredCapabilities $required_capabilities = null ) { throw new WebDriverException('Please use ChromeDriver::start() instead.'); } public static function createBySessionID( $session_id, $selenium_server_url = 'http://localhost:4444/wd/hub', $connection_timeout_in_ms = null, $request_timeout_in_ms = null ) { throw new WebDriverException('Please use ChromeDriver::start() instead.'); } } binary = $path; return $this; } public function addArguments(array $arguments) { $this->arguments = array_merge($this->arguments, $arguments); return $this; } public function addExtensions(array $paths) { foreach ($paths as $path) { $this->addExtension($path); } return $this; } public function addEncodedExtensions(array $encoded_extensions) { foreach ($encoded_extensions as $encoded_extension) { $this->addEncodedExtension($encoded_extension); } return $this; } public function setExperimentalOption($name, $value) { $this->experimentalOptions[$name] = $value; return $this; } public function toCapabilities() { $capabilities = DesiredCapabilities::chrome(); $capabilities->setCapability(self::CAPABILITY, $this); return $capabilities; } public function toArray() { $options = $this->experimentalOptions; $options['binary'] = $this->binary; if ($this->arguments) { $options['args'] = $this->arguments; } if ($this->extensions) { $options['extensions'] = $this->extensions; } return $options; } private function addExtension($path) { $this->addEncodedExtension(base64_encode(file_get_contents($path))); return $this; } private function addEncodedExtension($encoded_extension) { $this->extensions[] = $encoded_extension; return $this; } } validateCookieName($name); $this->validateCookieValue($value); $this->cookie['name'] = $name; $this->cookie['value'] = $value; } public static function createFromArray(array $cookieArray) { if (!isset($cookieArray['name'])) { throw new InvalidArgumentException('Cookie name should be set'); } if (!isset($cookieArray['value'])) { throw new InvalidArgumentException('Cookie value should be set'); } $cookie = new self($cookieArray['name'], $cookieArray['value']); if (isset($cookieArray['path'])) { $cookie->setPath($cookieArray['path']); } if (isset($cookieArray['domain'])) { $cookie->setDomain($cookieArray['domain']); } if (isset($cookieArray['expiry'])) { $cookie->setExpiry($cookieArray['expiry']); } if (isset($cookieArray['secure'])) { $cookie->setSecure($cookieArray['secure']); } if (isset($cookieArray['httpOnly'])) { $cookie->setHttpOnly($cookieArray['httpOnly']); } return $cookie; } public function getName() { return $this->offsetGet('name'); } public function getValue() { return $this->offsetGet('value'); } public function setPath($path) { $this->offsetSet('path', $path); } public function getPath() { return $this->offsetGet('path'); } public function setDomain($domain) { if (mb_strpos($domain, ':') !== false) { throw new InvalidArgumentException(sprintf('Cookie domain "%s" should not contain a port', $domain)); } $this->offsetSet('domain', $domain); } public function getDomain() { return $this->offsetGet('domain'); } public function setExpiry($expiry) { $this->offsetSet('expiry', (int) $expiry); } public function getExpiry() { return $this->offsetGet('expiry'); } public function setSecure($secure) { $this->offsetSet('secure', $secure); } public function isSecure() { return $this->offsetGet('secure'); } public function setHttpOnly($httpOnly) { $this->offsetSet('httpOnly', $httpOnly); } public function isHttpOnly() { return $this->offsetGet('httpOnly'); } public function toArray() { return $this->cookie; } public function offsetExists($offset) { return isset($this->cookie[$offset]); } public function offsetGet($offset) { return $this->offsetExists($offset) ? $this->cookie[$offset] : null; } public function offsetSet($offset, $value) { if ($value === null) { unset($this->cookie[$offset]); } else { $this->cookie[$offset] = $value; } } public function offsetUnset($offset) { unset($this->cookie[$offset]); } protected function validateCookieName($name) { if ($name === null || $name === '') { throw new InvalidArgumentException('Cookie name should be non-empty'); } if (mb_strpos($name, ';') !== false) { throw new InvalidArgumentException('Cookie name should not contain a ";"'); } } protected function validateCookieValue($value) { if ($value === null) { throw new InvalidArgumentException('Cookie value is required when setting a cookie'); } } } results = $results; } public function getResults() { return $this->results; } public static function throwException($status_code, $message, $results) { switch ($status_code) { case 1: throw new IndexOutOfBoundsException($message, $results); case 2: throw new NoCollectionException($message, $results); case 3: throw new NoStringException($message, $results); case 4: throw new NoStringLengthException($message, $results); case 5: throw new NoStringWrapperException($message, $results); case 6: throw new NoSuchDriverException($message, $results); case 7: throw new NoSuchElementException($message, $results); case 8: throw new NoSuchFrameException($message, $results); case 9: throw new UnknownCommandException($message, $results); case 10: throw new StaleElementReferenceException($message, $results); case 11: throw new ElementNotVisibleException($message, $results); case 12: throw new InvalidElementStateException($message, $results); case 13: throw new UnknownServerException($message, $results); case 14: throw new ExpectedException($message, $results); case 15: throw new ElementNotSelectableException($message, $results); case 16: throw new NoSuchDocumentException($message, $results); case 17: throw new UnexpectedJavascriptException($message, $results); case 18: throw new NoScriptResultException($message, $results); case 19: throw new XPathLookupException($message, $results); case 20: throw new NoSuchCollectionException($message, $results); case 21: throw new TimeOutException($message, $results); case 22: throw new NullPointerException($message, $results); case 23: throw new NoSuchWindowException($message, $results); case 24: throw new InvalidCookieDomainException($message, $results); case 25: throw new UnableToSetCookieException($message, $results); case 26: throw new UnexpectedAlertOpenException($message, $results); case 27: throw new NoAlertOpenException($message, $results); case 28: throw new ScriptTimeoutException($message, $results); case 29: throw new InvalidCoordinatesException($message, $results); case 30: throw new IMENotAvailableException($message, $results); case 31: throw new IMEEngineActivationFailedException($message, $results); case 32: throw new InvalidSelectorException($message, $results); case 33: throw new SessionNotCreatedException($message, $results); case 34: throw new MoveTargetOutOfBoundsException($message, $results); default: throw new UnrecognizedExceptionException($message, $results); } } } extensions[] = $extension; return $this; } public function addExtensionDatas($extension_datas) { if (!is_dir($extension_datas)) { return null; } $this->extensions_datas[basename($extension_datas)] = $extension_datas; return $this; } public function setRdfFile($rdf_file) { if (!is_file($rdf_file)) { return null; } $this->rdf_file = $rdf_file; return $this; } public function setPreference($key, $value) { if (is_string($value)) { $value = sprintf('"%s"', $value); } else { if (is_int($value)) { $value = sprintf('%d', $value); } else { if (is_bool($value)) { $value = $value ? 'true' : 'false'; } else { throw new WebDriverException( 'The value of the preference should be either a string, int or bool.' ); } } } $this->preferences[$key] = $value; return $this; } public function getPreference($key) { if (array_key_exists($key, $this->preferences)) { return $this->preferences[$key]; } return null; } public function encode() { $temp_dir = $this->createTempDirectory('WebDriverFirefoxProfile'); if (isset($this->rdf_file)) { copy($this->rdf_file, $temp_dir . DIRECTORY_SEPARATOR . 'mimeTypes.rdf'); } foreach ($this->extensions as $extension) { $this->installExtension($extension, $temp_dir); } foreach ($this->extensions_datas as $dirname => $extension_datas) { mkdir($temp_dir . DIRECTORY_SEPARATOR . $dirname); $iterator = new RecursiveIteratorIterator( new RecursiveDirectoryIterator($extension_datas, RecursiveDirectoryIterator::SKIP_DOTS), RecursiveIteratorIterator::SELF_FIRST ); foreach ($iterator as $item) { $target_dir = $temp_dir . DIRECTORY_SEPARATOR . $dirname . DIRECTORY_SEPARATOR . $iterator->getSubPathName(); if ($item->isDir()) { mkdir($target_dir); } else { copy($item, $target_dir); } } } $content = ''; foreach ($this->preferences as $key => $value) { $content .= sprintf("user_pref(\"%s\", %s);\n", $key, $value); } file_put_contents($temp_dir . '/user.js', $content); $zip = new ZipArchive(); $temp_zip = tempnam(sys_get_temp_dir(), 'WebDriverFirefoxProfileZip'); $zip->open($temp_zip, ZipArchive::CREATE); $dir = new RecursiveDirectoryIterator($temp_dir); $files = new RecursiveIteratorIterator($dir); $dir_prefix = preg_replace( '#\\\\#', '\\\\\\\\', $temp_dir . DIRECTORY_SEPARATOR ); foreach ($files as $name => $object) { if (is_dir($name)) { continue; } $path = preg_replace("#^{$dir_prefix}#", '', $name); $zip->addFile($name, $path); } $zip->close(); $profile = base64_encode(file_get_contents($temp_zip)); $this->deleteDirectory($temp_dir); unlink($temp_zip); return $profile; } private function installExtension($extension, $profile_dir) { $temp_dir = $this->createTempDirectory('WebDriverFirefoxProfileExtension'); $this->extractTo($extension, $temp_dir); $install_rdf_path = $temp_dir . '/install.rdf'; $xml = simplexml_load_file($install_rdf_path); $ns = $xml->getDocNamespaces(); $prefix = ''; if (!empty($ns)) { foreach ($ns as $key => $value) { if (mb_strpos($value, '//www.mozilla.org/2004/em-rdf') > 0) { if ($key != '') { $prefix = $key . ':'; } break; } } } $matches = []; preg_match('#<' . $prefix . 'id>([^<]+)#', $xml->asXML(), $matches); if (isset($matches[1])) { $ext_dir = $profile_dir . '/extensions/' . $matches[1]; mkdir($ext_dir, 0777, true); $this->extractTo($extension, $ext_dir); } else { $this->deleteDirectory($temp_dir); throw new WebDriverException('Cannot get the extension id from the install manifest.'); } $this->deleteDirectory($temp_dir); return $ext_dir; } private function createTempDirectory($prefix = '') { $temp_dir = tempnam(sys_get_temp_dir(), $prefix); if (file_exists($temp_dir)) { unlink($temp_dir); mkdir($temp_dir); if (!is_dir($temp_dir)) { throw new WebDriverException('Cannot create firefox profile.'); } } return $temp_dir; } private function deleteDirectory($directory) { $dir = new RecursiveDirectoryIterator($directory, FilesystemIterator::SKIP_DOTS); $paths = new RecursiveIteratorIterator($dir, RecursiveIteratorIterator::CHILD_FIRST); foreach ($paths as $path) { if ($path->isDir() && !$path->isLink()) { rmdir($path->getPathname()); } else { unlink($path->getPathname()); } } rmdir($directory); } private function extractTo($xpi, $target_dir) { $zip = new ZipArchive(); if (file_exists($xpi)) { if ($zip->open($xpi)) { $zip->extractTo($target_dir); $zip->close(); } else { throw new \Exception("Failed to open the firefox extension. '$xpi'"); } } else { throw new \Exception("Firefox extension doesn't exist. '$xpi'"); } return $this; } } mouse->mouseUp($this->getActionLocation()); } } mouse->click($this->getActionLocation()); } } mouse->mouseDown($this->getActionLocation()); } } mouse->contextClick($this->getActionLocation()); } } onScreen = $on_screen; $this->inViewPort = $in_view_port; $this->onPage = $on_page; $this->auxiliary = $auxiliary; } public function onScreen() { throw new UnsupportedOperationException( 'onScreen is planned but not yet supported by Selenium' ); } public function inViewPort() { return call_user_func($this->inViewPort); } public function onPage() { return call_user_func($this->onPage); } public function getAuxiliary() { return $this->auxiliary; } } mouse->doubleClick($this->getActionLocation()); } } focusOnElement(); $this->keyboard->pressKey($this->key); } } keyboard = $keyboard; $this->mouse = $mouse; $this->locationProvider = $location_provider; } protected function focusOnElement() { if ($this->locationProvider) { $this->mouse->click($this->locationProvider->getCoordinates()); } } } focusOnElement(); $this->keyboard->releaseKey($this->key); } } mouse = $mouse; $this->locationProvider = $location_provider; } protected function getActionLocation() { if ($this->locationProvider !== null) { return $this->locationProvider->getCoordinates(); } return null; } protected function moveToLocation() { $this->mouse->mouseMove($this->locationProvider); } } mouse->mouseMove($this->getActionLocation()); } } xOffset = $x_offset; $this->yOffset = $y_offset; } public function perform() { $this->mouse->mouseMove( $this->getActionLocation(), $this->xOffset, $this->yOffset ); } } keys = $keys; } public function perform() { $this->focusOnElement(); $this->keyboard->sendKeys($this->keys); } } key = $key; } } touchScreen->doubleTap($this->locationProvider); } } x = $x; $this->y = $y; parent::__construct($touch_screen); } public function perform() { $this->touchScreen->down($this->x, $this->y); } } x = $x; $this->y = $y; parent::__construct($touch_screen); } public function perform() { $this->touchScreen->flick($this->x, $this->y); } } x = $x; $this->y = $y; $this->speed = $speed; parent::__construct($touch_screen, $element); } public function perform() { $this->touchScreen->flickFromElement( $this->locationProvider, $this->x, $this->y, $this->speed ); } } touchScreen->longPress($this->locationProvider); } } x = $x; $this->y = $y; parent::__construct($touch_screen); } public function perform() { $this->touchScreen->move($this->x, $this->y); } } x = $x; $this->y = $y; parent::__construct($touch_screen); } public function perform() { $this->touchScreen->scroll($this->x, $this->y); } } x = $x; $this->y = $y; parent::__construct($touch_screen, $element); } public function perform() { $this->touchScreen->scrollFromElement( $this->locationProvider, $this->x, $this->y ); } } touchScreen->tap($this->locationProvider); } } touchScreen = $touch_screen; $this->locationProvider = $location_provider; } protected function getActionLocation() { return $this->locationProvider !== null ? $this->locationProvider->getCoordinates() : null; } } driver = $driver; $this->keyboard = $driver->getKeyboard(); $this->mouse = $driver->getMouse(); $this->action = new WebDriverCompositeAction(); } public function perform() { $this->action->perform(); } public function click(WebDriverElement $element = null) { $this->action->addAction( new WebDriverClickAction($this->mouse, $element) ); return $this; } public function clickAndHold(WebDriverElement $element = null) { $this->action->addAction( new WebDriverClickAndHoldAction($this->mouse, $element) ); return $this; } public function contextClick(WebDriverElement $element = null) { $this->action->addAction( new WebDriverContextClickAction($this->mouse, $element) ); return $this; } public function doubleClick(WebDriverElement $element = null) { $this->action->addAction( new WebDriverDoubleClickAction($this->mouse, $element) ); return $this; } public function dragAndDrop(WebDriverElement $source, WebDriverElement $target) { $this->action->addAction( new WebDriverClickAndHoldAction($this->mouse, $source) ); $this->action->addAction( new WebDriverMouseMoveAction($this->mouse, $target) ); $this->action->addAction( new WebDriverButtonReleaseAction($this->mouse, $target) ); return $this; } public function dragAndDropBy(WebDriverElement $source, $x_offset, $y_offset) { $this->action->addAction( new WebDriverClickAndHoldAction($this->mouse, $source) ); $this->action->addAction( new WebDriverMoveToOffsetAction($this->mouse, null, $x_offset, $y_offset) ); $this->action->addAction( new WebDriverButtonReleaseAction($this->mouse, null) ); return $this; } public function moveByOffset($x_offset, $y_offset) { $this->action->addAction( new WebDriverMoveToOffsetAction($this->mouse, null, $x_offset, $y_offset) ); return $this; } public function moveToElement(WebDriverElement $element, $x_offset = null, $y_offset = null) { $this->action->addAction(new WebDriverMoveToOffsetAction( $this->mouse, $element, $x_offset, $y_offset )); return $this; } public function release(WebDriverElement $element = null) { $this->action->addAction( new WebDriverButtonReleaseAction($this->mouse, $element) ); return $this; } public function keyDown(WebDriverElement $element = null, $key = null) { $this->action->addAction( new WebDriverKeyDownAction($this->keyboard, $this->mouse, $element, $key) ); return $this; } public function keyUp(WebDriverElement $element = null, $key = null) { $this->action->addAction( new WebDriverKeyUpAction($this->keyboard, $this->mouse, $element, $key) ); return $this; } public function sendKeys(WebDriverElement $element = null, $keys = null) { $this->action->addAction( new WebDriverSendKeysAction( $this->keyboard, $this->mouse, $element, $keys ) ); return $this; } } actions[] = $action; return $this; } public function getNumberOfActions() { return count($this->actions); } public function perform() { foreach ($this->actions as $action) { $action->perform(); } } } touchScreen = $driver->getTouch(); } public function tap(WebDriverElement $element) { $this->action->addAction( new WebDriverTapAction($this->touchScreen, $element) ); return $this; } public function down($x, $y) { $this->action->addAction( new WebDriverDownAction($this->touchScreen, $x, $y) ); return $this; } public function up($x, $y) { $this->action->addAction( new WebDriverUpAction($this->touchScreen, $x, $y) ); return $this; } public function move($x, $y) { $this->action->addAction( new WebDriverMoveAction($this->touchScreen, $x, $y) ); return $this; } public function scroll($x, $y) { $this->action->addAction( new WebDriverScrollAction($this->touchScreen, $x, $y) ); return $this; } public function scrollFromElement(WebDriverElement $element, $x, $y) { $this->action->addAction( new WebDriverScrollFromElementAction($this->touchScreen, $element, $x, $y) ); return $this; } public function doubleTap(WebDriverElement $element) { $this->action->addAction( new WebDriverDoubleTapAction($this->touchScreen, $element) ); return $this; } public function longPress(WebDriverElement $element) { $this->action->addAction( new WebDriverLongPressAction($this->touchScreen, $element) ); return $this; } public function flick($x, $y) { $this->action->addAction( new WebDriverFlickAction($this->touchScreen, $x, $y) ); return $this; } public function flickFromElement(WebDriverElement $element, $x, $y, $speed) { $this->action->addAction( new WebDriverFlickFromElementAction( $this->touchScreen, $element, $x, $y, $speed ) ); return $this; } } microtime(true)) { if ($this->getHTTPResponseCode($url) === 200) { return $this; } usleep(self::POLL_INTERVAL_MS); } throw new TimeOutException(sprintf( 'Timed out waiting for %s to become available after %d ms.', $url, $timeout_in_ms )); } public function waitUntilUnavailable($timeout_in_ms, $url) { $end = microtime(true) + $timeout_in_ms / 1000; while ($end > microtime(true)) { if ($this->getHTTPResponseCode($url) !== 200) { return $this; } usleep(self::POLL_INTERVAL_MS); } throw new TimeOutException(sprintf( 'Timed out waiting for %s to become unavailable after %d ms.', $url, $timeout_in_ms )); } private function getHTTPResponseCode($url) { $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); if (!defined('CURLOPT_CONNECTTIMEOUT_MS')) { define('CURLOPT_CONNECTTIMEOUT_MS', 156); } curl_setopt($ch, CURLOPT_CONNECTTIMEOUT_MS, self::CONNECT_TIMEOUT_MS); $code = null; try { curl_exec($ch); $info = curl_getinfo($ch); $code = $info['http_code']; } catch (Exception $e) { } curl_close($ch); return $code; } } capabilities = $capabilities; } public function getBrowserName() { return $this->get(WebDriverCapabilityType::BROWSER_NAME, ''); } public function setBrowserName($browser_name) { $this->set(WebDriverCapabilityType::BROWSER_NAME, $browser_name); return $this; } public function getVersion() { return $this->get(WebDriverCapabilityType::VERSION, ''); } public function setVersion($version) { $this->set(WebDriverCapabilityType::VERSION, $version); return $this; } public function getCapability($name) { return $this->get($name); } public function setCapability($name, $value) { $this->set($name, $value); return $this; } public function getPlatform() { return $this->get(WebDriverCapabilityType::PLATFORM, ''); } public function setPlatform($platform) { $this->set(WebDriverCapabilityType::PLATFORM, $platform); return $this; } public function is($capability_name) { return (bool) $this->get($capability_name); } public function isJavascriptEnabled() { return $this->get(WebDriverCapabilityType::JAVASCRIPT_ENABLED, false); } public function setJavascriptEnabled($enabled) { $browser = $this->getBrowserName(); if ($browser && $browser !== WebDriverBrowserType::HTMLUNIT) { throw new Exception( 'isJavascriptEnabled() is a htmlunit-only option. ' . 'See https://github.com/SeleniumHQ/selenium/wiki/DesiredCapabilities#read-write-capabilities.' ); } $this->set(WebDriverCapabilityType::JAVASCRIPT_ENABLED, $enabled); return $this; } public function toArray() { if (isset($this->capabilities[ChromeOptions::CAPABILITY]) && $this->capabilities[ChromeOptions::CAPABILITY] instanceof ChromeOptions ) { $this->capabilities[ChromeOptions::CAPABILITY] = $this->capabilities[ChromeOptions::CAPABILITY]->toArray(); } if (isset($this->capabilities[FirefoxDriver::PROFILE]) && $this->capabilities[FirefoxDriver::PROFILE] instanceof FirefoxProfile ) { $this->capabilities[FirefoxDriver::PROFILE] = $this->capabilities[FirefoxDriver::PROFILE]->encode(); } return $this->capabilities; } public static function android() { return new static([ WebDriverCapabilityType::BROWSER_NAME => WebDriverBrowserType::ANDROID, WebDriverCapabilityType::PLATFORM => WebDriverPlatform::ANDROID, ]); } public static function chrome() { return new static([ WebDriverCapabilityType::BROWSER_NAME => WebDriverBrowserType::CHROME, WebDriverCapabilityType::PLATFORM => WebDriverPlatform::ANY, ]); } public static function firefox() { $caps = new static([ WebDriverCapabilityType::BROWSER_NAME => WebDriverBrowserType::FIREFOX, WebDriverCapabilityType::PLATFORM => WebDriverPlatform::ANY, ]); $profile = new FirefoxProfile(); $profile->setPreference(FirefoxPreferences::READER_PARSE_ON_LOAD_ENABLED, false); $caps->setCapability(FirefoxDriver::PROFILE, $profile); return $caps; } public static function htmlUnit() { return new static([ WebDriverCapabilityType::BROWSER_NAME => WebDriverBrowserType::HTMLUNIT, WebDriverCapabilityType::PLATFORM => WebDriverPlatform::ANY, ]); } public static function htmlUnitWithJS() { $caps = new static([ WebDriverCapabilityType::BROWSER_NAME => WebDriverBrowserType::HTMLUNIT, WebDriverCapabilityType::PLATFORM => WebDriverPlatform::ANY, ]); return $caps->setJavascriptEnabled(true); } public static function internetExplorer() { return new static([ WebDriverCapabilityType::BROWSER_NAME => WebDriverBrowserType::IE, WebDriverCapabilityType::PLATFORM => WebDriverPlatform::WINDOWS, ]); } public static function microsoftEdge() { return new static([ WebDriverCapabilityType::BROWSER_NAME => WebDriverBrowserType::MICROSOFT_EDGE, WebDriverCapabilityType::PLATFORM => WebDriverPlatform::WINDOWS, ]); } public static function iphone() { return new static([ WebDriverCapabilityType::BROWSER_NAME => WebDriverBrowserType::IPHONE, WebDriverCapabilityType::PLATFORM => WebDriverPlatform::MAC, ]); } public static function ipad() { return new static([ WebDriverCapabilityType::BROWSER_NAME => WebDriverBrowserType::IPAD, WebDriverCapabilityType::PLATFORM => WebDriverPlatform::MAC, ]); } public static function opera() { return new static([ WebDriverCapabilityType::BROWSER_NAME => WebDriverBrowserType::OPERA, WebDriverCapabilityType::PLATFORM => WebDriverPlatform::ANY, ]); } public static function safari() { return new static([ WebDriverCapabilityType::BROWSER_NAME => WebDriverBrowserType::SAFARI, WebDriverCapabilityType::PLATFORM => WebDriverPlatform::ANY, ]); } public static function phantomjs() { return new static([ WebDriverCapabilityType::BROWSER_NAME => WebDriverBrowserType::PHANTOMJS, WebDriverCapabilityType::PLATFORM => WebDriverPlatform::ANY, ]); } private function set($key, $value) { $this->capabilities[$key] = $value; return $this; } private function get($key, $default = null) { return isset($this->capabilities[$key]) ? $this->capabilities[$key] : $default; } } ['method' => 'POST', 'url' => '/session/:sessionId/accept_alert'], DriverCommand::ADD_COOKIE => ['method' => 'POST', 'url' => '/session/:sessionId/cookie'], DriverCommand::CLEAR_ELEMENT => ['method' => 'POST', 'url' => '/session/:sessionId/element/:id/clear'], DriverCommand::CLICK_ELEMENT => ['method' => 'POST', 'url' => '/session/:sessionId/element/:id/click'], DriverCommand::CLOSE => ['method' => 'DELETE', 'url' => '/session/:sessionId/window'], DriverCommand::DELETE_ALL_COOKIES => ['method' => 'DELETE', 'url' => '/session/:sessionId/cookie'], DriverCommand::DELETE_COOKIE => ['method' => 'DELETE', 'url' => '/session/:sessionId/cookie/:name'], DriverCommand::DISMISS_ALERT => ['method' => 'POST', 'url' => '/session/:sessionId/dismiss_alert'], DriverCommand::ELEMENT_EQUALS => ['method' => 'GET', 'url' => '/session/:sessionId/element/:id/equals/:other'], DriverCommand::FIND_CHILD_ELEMENT => ['method' => 'POST', 'url' => '/session/:sessionId/element/:id/element'], DriverCommand::FIND_CHILD_ELEMENTS => ['method' => 'POST', 'url' => '/session/:sessionId/element/:id/elements'], DriverCommand::EXECUTE_SCRIPT => ['method' => 'POST', 'url' => '/session/:sessionId/execute'], DriverCommand::EXECUTE_ASYNC_SCRIPT => ['method' => 'POST', 'url' => '/session/:sessionId/execute_async'], DriverCommand::FIND_ELEMENT => ['method' => 'POST', 'url' => '/session/:sessionId/element'], DriverCommand::FIND_ELEMENTS => ['method' => 'POST', 'url' => '/session/:sessionId/elements'], DriverCommand::SWITCH_TO_FRAME => ['method' => 'POST', 'url' => '/session/:sessionId/frame'], DriverCommand::SWITCH_TO_WINDOW => ['method' => 'POST', 'url' => '/session/:sessionId/window'], DriverCommand::GET => ['method' => 'POST', 'url' => '/session/:sessionId/url'], DriverCommand::GET_ACTIVE_ELEMENT => ['method' => 'POST', 'url' => '/session/:sessionId/element/active'], DriverCommand::GET_ALERT_TEXT => ['method' => 'GET', 'url' => '/session/:sessionId/alert_text'], DriverCommand::GET_ALL_COOKIES => ['method' => 'GET', 'url' => '/session/:sessionId/cookie'], DriverCommand::GET_ALL_SESSIONS => ['method' => 'GET', 'url' => '/sessions'], DriverCommand::GET_AVAILABLE_LOG_TYPES => ['method' => 'GET', 'url' => '/session/:sessionId/log/types'], DriverCommand::GET_CURRENT_URL => ['method' => 'GET', 'url' => '/session/:sessionId/url'], DriverCommand::GET_CURRENT_WINDOW_HANDLE => ['method' => 'GET', 'url' => '/session/:sessionId/window_handle'], DriverCommand::GET_ELEMENT_ATTRIBUTE => [ 'method' => 'GET', 'url' => '/session/:sessionId/element/:id/attribute/:name', ], DriverCommand::GET_ELEMENT_VALUE_OF_CSS_PROPERTY => [ 'method' => 'GET', 'url' => '/session/:sessionId/element/:id/css/:propertyName', ], DriverCommand::GET_ELEMENT_LOCATION => [ 'method' => 'GET', 'url' => '/session/:sessionId/element/:id/location', ], DriverCommand::GET_ELEMENT_LOCATION_ONCE_SCROLLED_INTO_VIEW => [ 'method' => 'GET', 'url' => '/session/:sessionId/element/:id/location_in_view', ], DriverCommand::GET_ELEMENT_SIZE => ['method' => 'GET', 'url' => '/session/:sessionId/element/:id/size'], DriverCommand::GET_ELEMENT_TAG_NAME => ['method' => 'GET', 'url' => '/session/:sessionId/element/:id/name'], DriverCommand::GET_ELEMENT_TEXT => ['method' => 'GET', 'url' => '/session/:sessionId/element/:id/text'], DriverCommand::GET_LOG => ['method' => 'POST', 'url' => '/session/:sessionId/log'], DriverCommand::GET_PAGE_SOURCE => ['method' => 'GET', 'url' => '/session/:sessionId/source'], DriverCommand::GET_SCREEN_ORIENTATION => ['method' => 'GET', 'url' => '/session/:sessionId/orientation'], DriverCommand::GET_CAPABILITIES => ['method' => 'GET', 'url' => '/session/:sessionId'], DriverCommand::GET_TITLE => ['method' => 'GET', 'url' => '/session/:sessionId/title'], DriverCommand::GET_WINDOW_HANDLES => ['method' => 'GET', 'url' => '/session/:sessionId/window_handles'], DriverCommand::GET_WINDOW_POSITION => [ 'method' => 'GET', 'url' => '/session/:sessionId/window/:windowHandle/position', ], DriverCommand::GET_WINDOW_SIZE => ['method' => 'GET', 'url' => '/session/:sessionId/window/:windowHandle/size'], DriverCommand::GO_BACK => ['method' => 'POST', 'url' => '/session/:sessionId/back'], DriverCommand::GO_FORWARD => ['method' => 'POST', 'url' => '/session/:sessionId/forward'], DriverCommand::IS_ELEMENT_DISPLAYED => [ 'method' => 'GET', 'url' => '/session/:sessionId/element/:id/displayed', ], DriverCommand::IS_ELEMENT_ENABLED => ['method' => 'GET', 'url' => '/session/:sessionId/element/:id/enabled'], DriverCommand::IS_ELEMENT_SELECTED => ['method' => 'GET', 'url' => '/session/:sessionId/element/:id/selected'], DriverCommand::MAXIMIZE_WINDOW => [ 'method' => 'POST', 'url' => '/session/:sessionId/window/:windowHandle/maximize', ], DriverCommand::MOUSE_DOWN => ['method' => 'POST', 'url' => '/session/:sessionId/buttondown'], DriverCommand::MOUSE_UP => ['method' => 'POST', 'url' => '/session/:sessionId/buttonup'], DriverCommand::CLICK => ['method' => 'POST', 'url' => '/session/:sessionId/click'], DriverCommand::DOUBLE_CLICK => ['method' => 'POST', 'url' => '/session/:sessionId/doubleclick'], DriverCommand::MOVE_TO => ['method' => 'POST', 'url' => '/session/:sessionId/moveto'], DriverCommand::NEW_SESSION => ['method' => 'POST', 'url' => '/session'], DriverCommand::QUIT => ['method' => 'DELETE', 'url' => '/session/:sessionId'], DriverCommand::REFRESH => ['method' => 'POST', 'url' => '/session/:sessionId/refresh'], DriverCommand::UPLOAD_FILE => ['method' => 'POST', 'url' => '/session/:sessionId/file'], DriverCommand::SEND_KEYS_TO_ACTIVE_ELEMENT => ['method' => 'POST', 'url' => '/session/:sessionId/keys'], DriverCommand::SET_ALERT_VALUE => ['method' => 'POST', 'url' => '/session/:sessionId/alert_text'], DriverCommand::SEND_KEYS_TO_ELEMENT => ['method' => 'POST', 'url' => '/session/:sessionId/element/:id/value'], DriverCommand::IMPLICITLY_WAIT => ['method' => 'POST', 'url' => '/session/:sessionId/timeouts/implicit_wait'], DriverCommand::SET_SCREEN_ORIENTATION => ['method' => 'POST', 'url' => '/session/:sessionId/orientation'], DriverCommand::SET_TIMEOUT => ['method' => 'POST', 'url' => '/session/:sessionId/timeouts'], DriverCommand::SET_SCRIPT_TIMEOUT => ['method' => 'POST', 'url' => '/session/:sessionId/timeouts/async_script'], DriverCommand::SET_WINDOW_POSITION => [ 'method' => 'POST', 'url' => '/session/:sessionId/window/:windowHandle/position', ], DriverCommand::SET_WINDOW_SIZE => [ 'method' => 'POST', 'url' => '/session/:sessionId/window/:windowHandle/size', ], DriverCommand::SUBMIT_ELEMENT => ['method' => 'POST', 'url' => '/session/:sessionId/element/:id/submit'], DriverCommand::SCREENSHOT => ['method' => 'GET', 'url' => '/session/:sessionId/screenshot'], DriverCommand::TOUCH_SINGLE_TAP => ['method' => 'POST', 'url' => '/session/:sessionId/touch/click'], DriverCommand::TOUCH_DOWN => ['method' => 'POST', 'url' => '/session/:sessionId/touch/down'], DriverCommand::TOUCH_DOUBLE_TAP => ['method' => 'POST', 'url' => '/session/:sessionId/touch/doubleclick'], DriverCommand::TOUCH_FLICK => ['method' => 'POST', 'url' => '/session/:sessionId/touch/flick'], DriverCommand::TOUCH_LONG_PRESS => ['method' => 'POST', 'url' => '/session/:sessionId/touch/longclick'], DriverCommand::TOUCH_MOVE => ['method' => 'POST', 'url' => '/session/:sessionId/touch/move'], DriverCommand::TOUCH_SCROLL => ['method' => 'POST', 'url' => '/session/:sessionId/touch/scroll'], DriverCommand::TOUCH_UP => ['method' => 'POST', 'url' => '/session/:sessionId/touch/up'], ]; protected $url; protected $curl; public function __construct($url, $http_proxy = null, $http_proxy_port = null) { $this->url = $url; $this->curl = curl_init(); if (!empty($http_proxy)) { curl_setopt($this->curl, CURLOPT_PROXY, $http_proxy); if ($http_proxy_port !== null) { curl_setopt($this->curl, CURLOPT_PROXYPORT, $http_proxy_port); } } $matches = null; if (preg_match("/^(https?:\/\/)(.*):(.*)@(.*?)/U", $url, $matches)) { $this->url = $matches[1] . $matches[4]; $auth_creds = $matches[2] . ':' . $matches[3]; curl_setopt($this->curl, CURLOPT_HTTPAUTH, CURLAUTH_ANY); curl_setopt($this->curl, CURLOPT_USERPWD, $auth_creds); } curl_setopt($this->curl, CURLOPT_RETURNTRANSFER, true); curl_setopt($this->curl, CURLOPT_FOLLOWLOCATION, true); curl_setopt($this->curl, CURLOPT_HTTPHEADER, static::DEFAULT_HTTP_HEADERS); $this->setRequestTimeout(30000); $this->setConnectionTimeout(30000); } public function setConnectionTimeout($timeout_in_ms) { curl_setopt( $this->curl, 156, $timeout_in_ms ); return $this; } public function setRequestTimeout($timeout_in_ms) { curl_setopt( $this->curl, 155, $timeout_in_ms ); return $this; } public function execute(WebDriverCommand $command) { if (!isset(self::$commands[$command->getName()])) { throw new InvalidArgumentException($command->getName() . ' is not a valid command.'); } $raw = self::$commands[$command->getName()]; $http_method = $raw['method']; $url = $raw['url']; $url = str_replace(':sessionId', $command->getSessionID(), $url); $params = $command->getParameters(); foreach ($params as $name => $value) { if ($name[0] === ':') { $url = str_replace($name, $value, $url); unset($params[$name]); } } if ($params && is_array($params) && $http_method !== 'POST') { throw new BadMethodCallException(sprintf( 'The http method called for %s is %s but it has to be POST' . ' if you want to pass the JSON params %s', $url, $http_method, json_encode($params) )); } curl_setopt($this->curl, CURLOPT_URL, $this->url . $url); if ($command->getName() === DriverCommand::NEW_SESSION) { curl_setopt($this->curl, CURLOPT_POST, 1); } else { curl_setopt($this->curl, CURLOPT_CUSTOMREQUEST, $http_method); } if (in_array($http_method, ['POST', 'PUT'])) { curl_setopt($this->curl, CURLOPT_HTTPHEADER, array_merge(static::DEFAULT_HTTP_HEADERS, ['Expect:'])); } else { curl_setopt($this->curl, CURLOPT_HTTPHEADER, static::DEFAULT_HTTP_HEADERS); } $encoded_params = null; if ($http_method === 'POST' && $params && is_array($params)) { $encoded_params = json_encode($params); } elseif ($http_method === 'POST' && $encoded_params === null) { $encoded_params = json_encode(['_' => '_']); } curl_setopt($this->curl, CURLOPT_POSTFIELDS, $encoded_params); $raw_results = trim(curl_exec($this->curl)); if ($error = curl_error($this->curl)) { $msg = sprintf( 'Curl error thrown for http %s to %s', $http_method, $url ); if ($params && is_array($params)) { $msg .= sprintf(' with params: %s', json_encode($params)); } throw new WebDriverCurlException($msg . "\n\n" . $error); } $results = json_decode($raw_results, true); if ($results === null && json_last_error() !== JSON_ERROR_NONE) { throw new WebDriverException( sprintf( "JSON decoding of remote response failed.\n" . "Error code: %d\n" . "The response: '%s'\n", json_last_error(), $raw_results ) ); } $value = null; if (is_array($results) && array_key_exists('value', $results)) { $value = $results['value']; } $message = null; if (is_array($value) && array_key_exists('message', $value)) { $message = $value['message']; } $sessionId = null; if (is_array($results) && array_key_exists('sessionId', $results)) { $sessionId = $results['sessionId']; } $status = isset($results['status']) ? $results['status'] : 0; if ($status != 0) { WebDriverException::throwException($status, $message, $results); } $response = new WebDriverResponse($sessionId); return $response ->setStatus($status) ->setValue($value); } public function getAddressOfRemoteServer() { return $this->url; } } driver = $driver; } public function execute($command_name, array $parameters = []) { return $this->driver->execute($command_name, $parameters); } } executor = $executor; } public function sendKeys($keys) { $this->executor->execute(DriverCommand::SEND_KEYS_TO_ACTIVE_ELEMENT, [ 'value' => WebDriverKeys::encode($keys), ]); return $this; } public function pressKey($key) { $this->executor->execute(DriverCommand::SEND_KEYS_TO_ACTIVE_ELEMENT, [ 'value' => [(string) $key], ]); return $this; } public function releaseKey($key) { $this->executor->execute(DriverCommand::SEND_KEYS_TO_ACTIVE_ELEMENT, [ 'value' => [(string) $key], ]); return $this; } } executor = $executor; } public function click(WebDriverCoordinates $where = null) { $this->moveIfNeeded($where); $this->executor->execute(DriverCommand::CLICK, [ 'button' => 0, ]); return $this; } public function contextClick(WebDriverCoordinates $where = null) { $this->moveIfNeeded($where); $this->executor->execute(DriverCommand::CLICK, [ 'button' => 2, ]); return $this; } public function doubleClick(WebDriverCoordinates $where = null) { $this->moveIfNeeded($where); $this->executor->execute(DriverCommand::DOUBLE_CLICK); return $this; } public function mouseDown(WebDriverCoordinates $where = null) { $this->moveIfNeeded($where); $this->executor->execute(DriverCommand::MOUSE_DOWN); return $this; } public function mouseMove( WebDriverCoordinates $where = null, $x_offset = null, $y_offset = null ) { $params = []; if ($where !== null) { $params['element'] = $where->getAuxiliary(); } if ($x_offset !== null) { $params['xoffset'] = $x_offset; } if ($y_offset !== null) { $params['yoffset'] = $y_offset; } $this->executor->execute(DriverCommand::MOVE_TO, $params); return $this; } public function mouseUp(WebDriverCoordinates $where = null) { $this->moveIfNeeded($where); $this->executor->execute(DriverCommand::MOUSE_UP); return $this; } protected function moveIfNeeded(WebDriverCoordinates $where = null) { if ($where) { $this->mouseMove($where); } } } executor = $executor; $this->driver = $driver; } public function defaultContent() { $params = ['id' => null]; $this->executor->execute(DriverCommand::SWITCH_TO_FRAME, $params); return $this->driver; } public function frame($frame) { if ($frame instanceof WebDriverElement) { $id = ['ELEMENT' => $frame->getID()]; } else { $id = (string) $frame; } $params = ['id' => $id]; $this->executor->execute(DriverCommand::SWITCH_TO_FRAME, $params); return $this->driver; } public function window($handle) { $params = ['name' => (string) $handle]; $this->executor->execute(DriverCommand::SWITCH_TO_WINDOW, $params); return $this->driver; } public function alert() { return new WebDriverAlert($this->executor); } public function activeElement() { $response = $this->driver->execute(DriverCommand::GET_ACTIVE_ELEMENT, []); $method = new RemoteExecuteMethod($this->driver); return new RemoteWebElement($method, $response['ELEMENT']); } } executor = $executor; } public function tap(WebDriverElement $element) { $this->executor->execute( DriverCommand::TOUCH_SINGLE_TAP, ['element' => $element->getID()] ); return $this; } public function doubleTap(WebDriverElement $element) { $this->executor->execute( DriverCommand::TOUCH_DOUBLE_TAP, ['element' => $element->getID()] ); return $this; } public function down($x, $y) { $this->executor->execute(DriverCommand::TOUCH_DOWN, [ 'x' => $x, 'y' => $y, ]); return $this; } public function flick($xspeed, $yspeed) { $this->executor->execute(DriverCommand::TOUCH_FLICK, [ 'xspeed' => $xspeed, 'yspeed' => $yspeed, ]); return $this; } public function flickFromElement(WebDriverElement $element, $xoffset, $yoffset, $speed) { $this->executor->execute(DriverCommand::TOUCH_FLICK, [ 'xoffset' => $xoffset, 'yoffset' => $yoffset, 'element' => $element->getID(), 'speed' => $speed, ]); return $this; } public function longPress(WebDriverElement $element) { $this->executor->execute( DriverCommand::TOUCH_LONG_PRESS, ['element' => $element->getID()] ); return $this; } public function move($x, $y) { $this->executor->execute(DriverCommand::TOUCH_MOVE, [ 'x' => $x, 'y' => $y, ]); return $this; } public function scroll($xoffset, $yoffset) { $this->executor->execute(DriverCommand::TOUCH_SCROLL, [ 'xoffset' => $xoffset, 'yoffset' => $yoffset, ]); return $this; } public function scrollFromElement(WebDriverElement $element, $xoffset, $yoffset) { $this->executor->execute(DriverCommand::TOUCH_SCROLL, [ 'element' => $element->getID(), 'xoffset' => $xoffset, 'yoffset' => $yoffset, ]); return $this; } public function up($x, $y) { $this->executor->execute(DriverCommand::TOUCH_UP, [ 'x' => $x, 'y' => $y, ]); return $this; } } executor = $commandExecutor; $this->sessionID = $sessionId; if ($capabilities !== null) { $this->capabilities = $capabilities; } } public static function create( $selenium_server_url = 'http://localhost:4444/wd/hub', $desired_capabilities = null, $connection_timeout_in_ms = null, $request_timeout_in_ms = null, $http_proxy = null, $http_proxy_port = null, DesiredCapabilities $required_capabilities = null ) { $selenium_server_url = preg_replace('#/+$#', '', $selenium_server_url); $desired_capabilities = self::castToDesiredCapabilitiesObject($desired_capabilities); if ($desired_capabilities->getBrowserName() === WebDriverBrowserType::CHROME && mb_strpos($selenium_server_url, 'browserstack') === false ) { $currentChromeOptions = $desired_capabilities->getCapability(ChromeOptions::CAPABILITY); $chromeOptions = !empty($currentChromeOptions) ? $currentChromeOptions : new ChromeOptions(); if ($chromeOptions instanceof ChromeOptions && !isset($chromeOptions->toArray()['w3c'])) { $chromeOptions->setExperimentalOption('w3c', false); } elseif (is_array($chromeOptions) && !isset($chromeOptions['w3c'])) { $chromeOptions['w3c'] = false; } $desired_capabilities->setCapability(ChromeOptions::CAPABILITY, $chromeOptions); } $executor = new HttpCommandExecutor($selenium_server_url, $http_proxy, $http_proxy_port); if ($connection_timeout_in_ms !== null) { $executor->setConnectionTimeout($connection_timeout_in_ms); } if ($request_timeout_in_ms !== null) { $executor->setRequestTimeout($request_timeout_in_ms); } if ($required_capabilities !== null) { $desired_capabilities->setCapability('requiredCapabilities', $required_capabilities->toArray()); } $command = new WebDriverCommand( null, DriverCommand::NEW_SESSION, ['desiredCapabilities' => $desired_capabilities->toArray()] ); $response = $executor->execute($command); $returnedCapabilities = new DesiredCapabilities($response->getValue()); $driver = new static($executor, $response->getSessionID(), $returnedCapabilities); return $driver; } public static function createBySessionID( $session_id, $selenium_server_url = 'http://localhost:4444/wd/hub', $connection_timeout_in_ms = null, $request_timeout_in_ms = null ) { $executor = new HttpCommandExecutor($selenium_server_url); if ($connection_timeout_in_ms !== null) { $executor->setConnectionTimeout($connection_timeout_in_ms); } if ($request_timeout_in_ms !== null) { $executor->setRequestTimeout($request_timeout_in_ms); } return new static($executor, $session_id); } public function close() { $this->execute(DriverCommand::CLOSE, []); return $this; } public function findElement(WebDriverBy $by) { $params = ['using' => $by->getMechanism(), 'value' => $by->getValue()]; $raw_element = $this->execute( DriverCommand::FIND_ELEMENT, $params ); return $this->newElement($raw_element['ELEMENT']); } public function findElements(WebDriverBy $by) { $params = ['using' => $by->getMechanism(), 'value' => $by->getValue()]; $raw_elements = $this->execute( DriverCommand::FIND_ELEMENTS, $params ); $elements = []; foreach ($raw_elements as $raw_element) { $elements[] = $this->newElement($raw_element['ELEMENT']); } return $elements; } public function get($url) { $params = ['url' => (string) $url]; $this->execute(DriverCommand::GET, $params); return $this; } public function getCurrentURL() { return $this->execute(DriverCommand::GET_CURRENT_URL); } public function getPageSource() { return $this->execute(DriverCommand::GET_PAGE_SOURCE); } public function getTitle() { return $this->execute(DriverCommand::GET_TITLE); } public function getWindowHandle() { return $this->execute( DriverCommand::GET_CURRENT_WINDOW_HANDLE, [] ); } public function getWindowHandles() { return $this->execute(DriverCommand::GET_WINDOW_HANDLES, []); } public function quit() { $this->execute(DriverCommand::QUIT); $this->executor = null; } public function executeScript($script, array $arguments = []) { $params = [ 'script' => $script, 'args' => $this->prepareScriptArguments($arguments), ]; return $this->execute(DriverCommand::EXECUTE_SCRIPT, $params); } public function executeAsyncScript($script, array $arguments = []) { $params = [ 'script' => $script, 'args' => $this->prepareScriptArguments($arguments), ]; return $this->execute( DriverCommand::EXECUTE_ASYNC_SCRIPT, $params ); } public function takeScreenshot($save_as = null) { $screenshot = base64_decode( $this->execute(DriverCommand::SCREENSHOT) ); if ($save_as) { file_put_contents($save_as, $screenshot); } return $screenshot; } public function wait($timeout_in_second = 30, $interval_in_millisecond = 250) { return new WebDriverWait( $this, $timeout_in_second, $interval_in_millisecond ); } public function manage() { return new WebDriverOptions($this->getExecuteMethod()); } public function navigate() { return new WebDriverNavigation($this->getExecuteMethod()); } public function switchTo() { return new RemoteTargetLocator($this->getExecuteMethod(), $this); } public function getMouse() { if (!$this->mouse) { $this->mouse = new RemoteMouse($this->getExecuteMethod()); } return $this->mouse; } public function getKeyboard() { if (!$this->keyboard) { $this->keyboard = new RemoteKeyboard($this->getExecuteMethod()); } return $this->keyboard; } public function getTouch() { if (!$this->touch) { $this->touch = new RemoteTouchScreen($this->getExecuteMethod()); } return $this->touch; } public function action() { return new WebDriverActions($this); } public function setCommandExecutor(WebDriverCommandExecutor $executor) { $this->executor = $executor; return $this; } public function getCommandExecutor() { return $this->executor; } public function setSessionID($session_id) { $this->sessionID = $session_id; return $this; } public function getSessionID() { return $this->sessionID; } public function getCapabilities() { return $this->capabilities; } public static function getAllSessions($selenium_server_url = 'http://localhost:4444/wd/hub', $timeout_in_ms = 30000) { $executor = new HttpCommandExecutor($selenium_server_url); $executor->setConnectionTimeout($timeout_in_ms); $command = new WebDriverCommand( null, DriverCommand::GET_ALL_SESSIONS, [] ); return $executor->execute($command)->getValue(); } public function execute($command_name, $params = []) { $command = new WebDriverCommand( $this->sessionID, $command_name, $params ); if ($this->executor) { $response = $this->executor->execute($command); return $response->getValue(); } return null; } protected function prepareScriptArguments(array $arguments) { $args = []; foreach ($arguments as $key => $value) { if ($value instanceof WebDriverElement) { $args[$key] = ['ELEMENT' => $value->getID()]; } else { if (is_array($value)) { $value = $this->prepareScriptArguments($value); } $args[$key] = $value; } } return $args; } protected function getExecuteMethod() { if (!$this->executeMethod) { $this->executeMethod = new RemoteExecuteMethod($this); } return $this->executeMethod; } protected function newElement($id) { return new RemoteWebElement($this->getExecuteMethod(), $id); } protected static function castToDesiredCapabilitiesObject($desired_capabilities = null) { if ($desired_capabilities === null) { return new DesiredCapabilities(); } if (is_array($desired_capabilities)) { return new DesiredCapabilities($desired_capabilities); } return $desired_capabilities; } } executor = $executor; $this->id = $id; $this->fileDetector = new UselessFileDetector(); } public function clear() { $this->executor->execute( DriverCommand::CLEAR_ELEMENT, [':id' => $this->id] ); return $this; } public function click() { $this->executor->execute( DriverCommand::CLICK_ELEMENT, [':id' => $this->id] ); return $this; } public function findElement(WebDriverBy $by) { $params = [ 'using' => $by->getMechanism(), 'value' => $by->getValue(), ':id' => $this->id, ]; $raw_element = $this->executor->execute( DriverCommand::FIND_CHILD_ELEMENT, $params ); return $this->newElement($raw_element['ELEMENT']); } public function findElements(WebDriverBy $by) { $params = [ 'using' => $by->getMechanism(), 'value' => $by->getValue(), ':id' => $this->id, ]; $raw_elements = $this->executor->execute( DriverCommand::FIND_CHILD_ELEMENTS, $params ); $elements = []; foreach ($raw_elements as $raw_element) { $elements[] = $this->newElement($raw_element['ELEMENT']); } return $elements; } public function getAttribute($attribute_name) { $params = [ ':name' => $attribute_name, ':id' => $this->id, ]; return $this->executor->execute( DriverCommand::GET_ELEMENT_ATTRIBUTE, $params ); } public function getCSSValue($css_property_name) { $params = [ ':propertyName' => $css_property_name, ':id' => $this->id, ]; return $this->executor->execute( DriverCommand::GET_ELEMENT_VALUE_OF_CSS_PROPERTY, $params ); } public function getLocation() { $location = $this->executor->execute( DriverCommand::GET_ELEMENT_LOCATION, [':id' => $this->id] ); return new WebDriverPoint($location['x'], $location['y']); } public function getLocationOnScreenOnceScrolledIntoView() { $location = $this->executor->execute( DriverCommand::GET_ELEMENT_LOCATION_ONCE_SCROLLED_INTO_VIEW, [':id' => $this->id] ); return new WebDriverPoint($location['x'], $location['y']); } public function getCoordinates() { $element = $this; $on_screen = null; $in_view_port = function () use ($element) { return $element->getLocationOnScreenOnceScrolledIntoView(); }; $on_page = function () use ($element) { return $element->getLocation(); }; $auxiliary = $this->getID(); return new WebDriverCoordinates( $on_screen, $in_view_port, $on_page, $auxiliary ); } public function getSize() { $size = $this->executor->execute( DriverCommand::GET_ELEMENT_SIZE, [':id' => $this->id] ); return new WebDriverDimension($size['width'], $size['height']); } public function getTagName() { return mb_strtolower($this->executor->execute( DriverCommand::GET_ELEMENT_TAG_NAME, [':id' => $this->id] )); } public function getText() { return $this->executor->execute( DriverCommand::GET_ELEMENT_TEXT, [':id' => $this->id] ); } public function isDisplayed() { return $this->executor->execute( DriverCommand::IS_ELEMENT_DISPLAYED, [':id' => $this->id] ); } public function isEnabled() { return $this->executor->execute( DriverCommand::IS_ELEMENT_ENABLED, [':id' => $this->id] ); } public function isSelected() { return $this->executor->execute( DriverCommand::IS_ELEMENT_SELECTED, [':id' => $this->id] ); } public function sendKeys($value) { $local_file = $this->fileDetector->getLocalFile($value); if ($local_file === null) { $params = [ 'value' => WebDriverKeys::encode($value), ':id' => $this->id, ]; $this->executor->execute(DriverCommand::SEND_KEYS_TO_ELEMENT, $params); } else { $remote_path = $this->upload($local_file); $params = [ 'value' => WebDriverKeys::encode($remote_path), ':id' => $this->id, ]; $this->executor->execute(DriverCommand::SEND_KEYS_TO_ELEMENT, $params); } return $this; } public function setFileDetector(FileDetector $detector) { $this->fileDetector = $detector; return $this; } public function submit() { $this->executor->execute( DriverCommand::SUBMIT_ELEMENT, [':id' => $this->id] ); return $this; } public function getID() { return $this->id; } public function equals(WebDriverElement $other) { return $this->executor->execute(DriverCommand::ELEMENT_EQUALS, [ ':id' => $this->id, ':other' => $other->getID(), ]); } protected function newElement($id) { return new static($this->executor, $id); } protected function upload($local_file) { if (!is_file($local_file)) { throw new WebDriverException('You may only upload files: ' . $local_file); } $temp_zip = tempnam(sys_get_temp_dir(), 'WebDriverZip'); $zip = new ZipArchive(); if ($zip->open($temp_zip, ZipArchive::CREATE) !== true) { return false; } $info = pathinfo($local_file); $file_name = $info['basename']; $zip->addFile($local_file, $file_name); $zip->close(); $params = [ 'file' => base64_encode(file_get_contents($temp_zip)), ]; $remote_path = $this->executor->execute( DriverCommand::UPLOAD_FILE, $params ); unlink($temp_zip); return $remote_path; } } getURL()); $this->service = $service; } public function execute(WebDriverCommand $command) { if ($command->getName() === DriverCommand::NEW_SESSION) { $this->service->start(); } try { $value = parent::execute($command); if ($command->getName() === DriverCommand::QUIT) { $this->service->stop(); } return $value; } catch (\Exception $e) { if (!$this->service->isRunning()) { throw new WebDriverException('The driver server has died.'); } throw $e; } } } executable = self::checkExecutable($executable); $this->url = sprintf('http://localhost:%d', $port); $this->args = $args; $this->environment = $environment ?: $_ENV; } public function getURL() { return $this->url; } public function start() { if ($this->process !== null) { return $this; } $this->process = $this->createProcess(); $this->process->start(); $checker = new URLChecker(); $checker->waitUntilAvailable(20 * 1000, $this->url . '/status'); return $this; } public function stop() { if ($this->process === null) { return $this; } $this->process->stop(); $this->process = null; $checker = new URLChecker(); $checker->waitUntilUnavailable(3 * 1000, $this->url . '/shutdown'); return $this; } public function isRunning() { if ($this->process === null) { return false; } return $this->process->isRunning(); } protected static function checkExecutable($executable) { if (!is_file($executable)) { throw new Exception("'$executable' is not a file."); } if (!is_executable($executable)) { throw new Exception("'$executable' is not executable."); } return $executable; } private function createProcess() { if (class_exists(ProcessBuilder::class) && false === mb_strpos('@deprecated', (new \ReflectionClass(ProcessBuilder::class))->getDocComment()) ) { $processBuilder = (new ProcessBuilder()) ->setPrefix($this->executable) ->setArguments($this->args) ->addEnvironmentVariables($this->environment); return $processBuilder->getProcess(); } $commandLine = array_merge([$this->executable], $this->args); return new Process($commandLine, null, $this->environment); } } sessionID = $session_id; $this->name = $name; $this->parameters = $parameters; } public function getName() { return $this->name; } public function getSessionID() { return $this->sessionID; } public function getParameters() { return $this->parameters; } } sessionID = $session_id; } public function getStatus() { return $this->status; } public function setStatus($status) { $this->status = $status; return $this; } public function getValue() { return $this->value; } public function setValue($value) { $this->value = $value; return $this; } public function getSessionID() { return $this->sessionID; } public function setSessionID($session_id) { $this->sessionID = $session_id; return $this; } } dispatcher = $dispatcher ?: new WebDriverDispatcher(); if (!$this->dispatcher->getDefaultDriver()) { $this->dispatcher->setDefaultDriver($this); } $this->driver = $driver; } public function getDispatcher() { return $this->dispatcher; } public function getWebDriver() { return $this->driver; } public function get($url) { $this->dispatch('beforeNavigateTo', $url, $this); try { $this->driver->get($url); } catch (WebDriverException $exception) { $this->dispatchOnException($exception); throw $exception; } $this->dispatch('afterNavigateTo', $url, $this); return $this; } public function findElements(WebDriverBy $by) { $this->dispatch('beforeFindBy', $by, null, $this); $elements = []; try { foreach ($this->driver->findElements($by) as $element) { $elements[] = $this->newElement($element); } } catch (WebDriverException $exception) { $this->dispatchOnException($exception); throw $exception; } $this->dispatch('afterFindBy', $by, null, $this); return $elements; } public function findElement(WebDriverBy $by) { $this->dispatch('beforeFindBy', $by, null, $this); try { $element = $this->newElement($this->driver->findElement($by)); } catch (WebDriverException $exception) { $this->dispatchOnException($exception); throw $exception; } $this->dispatch('afterFindBy', $by, null, $this); return $element; } public function executeScript($script, array $arguments = []) { if (!$this->driver instanceof JavaScriptExecutor) { throw new UnsupportedOperationException( 'driver does not implement JavaScriptExecutor' ); } $this->dispatch('beforeScript', $script, $this); try { $result = $this->driver->executeScript($script, $arguments); } catch (WebDriverException $exception) { $this->dispatchOnException($exception); throw $exception; } $this->dispatch('afterScript', $script, $this); return $result; } public function executeAsyncScript($script, array $arguments = []) { if (!$this->driver instanceof JavaScriptExecutor) { throw new UnsupportedOperationException( 'driver does not implement JavaScriptExecutor' ); } $this->dispatch('beforeScript', $script, $this); try { $result = $this->driver->executeAsyncScript($script, $arguments); } catch (WebDriverException $exception) { $this->dispatchOnException($exception); throw $exception; } $this->dispatch('afterScript', $script, $this); return $result; } public function close() { try { $this->driver->close(); return $this; } catch (WebDriverException $exception) { $this->dispatchOnException($exception); throw $exception; } } public function getCurrentURL() { try { return $this->driver->getCurrentURL(); } catch (WebDriverException $exception) { $this->dispatchOnException($exception); throw $exception; } } public function getPageSource() { try { return $this->driver->getPageSource(); } catch (WebDriverException $exception) { $this->dispatchOnException($exception); throw $exception; } } public function getTitle() { try { return $this->driver->getTitle(); } catch (WebDriverException $exception) { $this->dispatchOnException($exception); throw $exception; } } public function getWindowHandle() { try { return $this->driver->getWindowHandle(); } catch (WebDriverException $exception) { $this->dispatchOnException($exception); throw $exception; } } public function getWindowHandles() { try { return $this->driver->getWindowHandles(); } catch (WebDriverException $exception) { $this->dispatchOnException($exception); throw $exception; } } public function quit() { try { $this->driver->quit(); } catch (WebDriverException $exception) { $this->dispatchOnException($exception); throw $exception; } } public function takeScreenshot($save_as = null) { try { return $this->driver->takeScreenshot($save_as); } catch (WebDriverException $exception) { $this->dispatchOnException($exception); throw $exception; } } public function wait($timeout_in_second = 30, $interval_in_millisecond = 250) { try { return $this->driver->wait($timeout_in_second, $interval_in_millisecond); } catch (WebDriverException $exception) { $this->dispatchOnException($exception); throw $exception; } } public function manage() { try { return $this->driver->manage(); } catch (WebDriverException $exception) { $this->dispatchOnException($exception); throw $exception; } } public function navigate() { try { return new EventFiringWebDriverNavigation( $this->driver->navigate(), $this->getDispatcher() ); } catch (WebDriverException $exception) { $this->dispatchOnException($exception); throw $exception; } } public function switchTo() { try { return $this->driver->switchTo(); } catch (WebDriverException $exception) { $this->dispatchOnException($exception); throw $exception; } } public function getTouch() { try { return $this->driver->getTouch(); } catch (WebDriverException $exception) { $this->dispatchOnException($exception); throw $exception; } } public function execute($name, $params) { try { return $this->driver->execute($name, $params); } catch (WebDriverException $exception) { $this->dispatchOnException($exception); throw $exception; } } protected function newElement(WebDriverElement $element) { return new EventFiringWebElement($element, $this->getDispatcher()); } protected function dispatch($method, ...$arguments) { if (!$this->dispatcher) { return; } $this->dispatcher->dispatch($method, $arguments); } protected function dispatchOnException(WebDriverException $exception) { $this->dispatch('onException', $exception, $this); } } navigator = $navigator; $this->dispatcher = $dispatcher; } public function getDispatcher() { return $this->dispatcher; } public function getNavigator() { return $this->navigator; } public function back() { $this->dispatch( 'beforeNavigateBack', $this->getDispatcher()->getDefaultDriver() ); try { $this->navigator->back(); } catch (WebDriverException $exception) { $this->dispatchOnException($exception); } $this->dispatch( 'afterNavigateBack', $this->getDispatcher()->getDefaultDriver() ); return $this; } public function forward() { $this->dispatch( 'beforeNavigateForward', $this->getDispatcher()->getDefaultDriver() ); try { $this->navigator->forward(); } catch (WebDriverException $exception) { $this->dispatchOnException($exception); } $this->dispatch( 'afterNavigateForward', $this->getDispatcher()->getDefaultDriver() ); return $this; } public function refresh() { try { $this->navigator->refresh(); return $this; } catch (WebDriverException $exception) { $this->dispatchOnException($exception); throw $exception; } } public function to($url) { $this->dispatch( 'beforeNavigateTo', $url, $this->getDispatcher()->getDefaultDriver() ); try { $this->navigator->to($url); } catch (WebDriverException $exception) { $this->dispatchOnException($exception); throw $exception; } $this->dispatch( 'afterNavigateTo', $url, $this->getDispatcher()->getDefaultDriver() ); return $this; } protected function dispatch($method, ...$arguments) { if (!$this->dispatcher) { return; } $this->dispatcher->dispatch($method, $arguments); } protected function dispatchOnException(WebDriverException $exception) { $this->dispatch('onException', $exception); } } element = $element; $this->dispatcher = $dispatcher; } public function getDispatcher() { return $this->dispatcher; } public function getElement() { return $this->element; } public function sendKeys($value) { $this->dispatch('beforeChangeValueOf', $this); try { $this->element->sendKeys($value); } catch (WebDriverException $exception) { $this->dispatchOnException($exception); throw $exception; } $this->dispatch('afterChangeValueOf', $this); return $this; } public function click() { $this->dispatch('beforeClickOn', $this); try { $this->element->click(); } catch (WebDriverException $exception) { $this->dispatchOnException($exception); throw $exception; } $this->dispatch('afterClickOn', $this); return $this; } public function findElement(WebDriverBy $by) { $this->dispatch( 'beforeFindBy', $by, $this, $this->dispatcher->getDefaultDriver() ); try { $element = $this->newElement($this->element->findElement($by)); } catch (WebDriverException $exception) { $this->dispatchOnException($exception); throw $exception; } $this->dispatch( 'afterFindBy', $by, $this, $this->dispatcher->getDefaultDriver() ); return $element; } public function findElements(WebDriverBy $by) { $this->dispatch( 'beforeFindBy', $by, $this, $this->dispatcher->getDefaultDriver() ); try { $elements = []; foreach ($this->element->findElements($by) as $element) { $elements[] = $this->newElement($element); } } catch (WebDriverException $exception) { $this->dispatchOnException($exception); throw $exception; } $this->dispatch( 'afterFindBy', $by, $this, $this->dispatcher->getDefaultDriver() ); return $elements; } public function clear() { try { $this->element->clear(); return $this; } catch (WebDriverException $exception) { $this->dispatchOnException($exception); throw $exception; } } public function getAttribute($attribute_name) { try { return $this->element->getAttribute($attribute_name); } catch (WebDriverException $exception) { $this->dispatchOnException($exception); throw $exception; } } public function getCSSValue($css_property_name) { try { return $this->element->getCSSValue($css_property_name); } catch (WebDriverException $exception) { $this->dispatchOnException($exception); throw $exception; } } public function getLocation() { try { return $this->element->getLocation(); } catch (WebDriverException $exception) { $this->dispatchOnException($exception); throw $exception; } } public function getLocationOnScreenOnceScrolledIntoView() { try { return $this->element->getLocationOnScreenOnceScrolledIntoView(); } catch (WebDriverException $exception) { $this->dispatchOnException($exception); throw $exception; } } public function getCoordinates() { try { return $this->element->getCoordinates(); } catch (WebDriverException $exception) { $this->dispatchOnException($exception); throw $exception; } } public function getSize() { try { return $this->element->getSize(); } catch (WebDriverException $exception) { $this->dispatchOnException($exception); throw $exception; } } public function getTagName() { try { return $this->element->getTagName(); } catch (WebDriverException $exception) { $this->dispatchOnException($exception); throw $exception; } } public function getText() { try { return $this->element->getText(); } catch (WebDriverException $exception) { $this->dispatchOnException($exception); throw $exception; } } public function isDisplayed() { try { return $this->element->isDisplayed(); } catch (WebDriverException $exception) { $this->dispatchOnException($exception); throw $exception; } } public function isEnabled() { try { return $this->element->isEnabled(); } catch (WebDriverException $exception) { $this->dispatchOnException($exception); throw $exception; } } public function isSelected() { try { return $this->element->isSelected(); } catch (WebDriverException $exception) { $this->dispatchOnException($exception); throw $exception; } } public function submit() { try { $this->element->submit(); return $this; } catch (WebDriverException $exception) { $this->dispatchOnException($exception); throw $exception; } } public function getID() { try { return $this->element->getID(); } catch (WebDriverException $exception) { $this->dispatchOnException($exception); throw $exception; } } public function equals(WebDriverElement $other) { try { return $this->element->equals($other); } catch (WebDriverException $exception) { $this->dispatchOnException($exception); throw $exception; } } protected function dispatchOnException(WebDriverException $exception) { $this->dispatch( 'onException', $exception, $this->dispatcher->getDefaultDriver() ); } protected function dispatch($method, ...$arguments) { if (!$this->dispatcher) { return; } $this->dispatcher->dispatch($method, $arguments); } protected function newElement(WebDriverElement $element) { return new static($element, $this->getDispatcher()); } } executor = $executor; } public function accept() { $this->executor->execute(DriverCommand::ACCEPT_ALERT); return $this; } public function dismiss() { $this->executor->execute(DriverCommand::DISMISS_ALERT); return $this; } public function getText() { return $this->executor->execute(DriverCommand::GET_ALERT_TEXT); } public function sendKeys($value) { $this->executor->execute( DriverCommand::SET_ALERT_VALUE, ['text' => $value] ); return $this; } } mechanism = $mechanism; $this->value = $value; } public function getMechanism() { return $this->mechanism; } public function getValue() { return $this->value; } public static function className($class_name) { return new static('class name', $class_name); } public static function cssSelector($css_selector) { return new static('css selector', $css_selector); } public static function id($id) { return new static('id', $id); } public static function name($name) { return new static('name', $name); } public static function linkText($link_text) { return new static('link text', $link_text); } public static function partialLinkText($partial_link_text) { return new static('partial link text', $partial_link_text); } public static function tagName($tag_name) { return new static('tag name', $tag_name); } public static function xpath($xpath) { return new static('xpath', $xpath); } } type = $element->getAttribute('type'); if ($this->type !== 'checkbox') { throw new WebDriverException('The input must be of type "checkbox".'); } } public function isMultiple() { return true; } public function deselectAll() { foreach ($this->getRelatedElements() as $checkbox) { $this->deselectOption($checkbox); } } public function deselectByIndex($index) { $this->byIndex($index, false); } public function deselectByValue($value) { $this->byValue($value, false); } public function deselectByVisibleText($text) { $this->byVisibleText($text, false, false); } public function deselectByVisiblePartialText($text) { $this->byVisibleText($text, true, false); } } width = $width; $this->height = $height; } public function getHeight() { return $this->height; } public function getWidth() { return $this->width; } public function equals(self $dimension) { return $this->height === $dimension->getHeight() && $this->width === $dimension->getWidth(); } } driver = $driver; return $this; } public function getDefaultDriver() { return $this->driver; } public function register(WebDriverEventListener $listener) { $this->listeners[] = $listener; return $this; } public function unregister(WebDriverEventListener $listener) { $key = array_search($listener, $this->listeners, true); if ($key !== false) { unset($this->listeners[$key]); } return $this; } public function dispatch($method, $arguments) { foreach ($this->listeners as $listener) { call_user_func_array([$listener, $method], $arguments); } return $this; } } apply = $apply; } public function getApply() { return $this->apply; } public static function titleIs($title) { return new static( function (WebDriver $driver) use ($title) { return $title === $driver->getTitle(); } ); } public static function titleContains($title) { return new static( function (WebDriver $driver) use ($title) { return mb_strpos($driver->getTitle(), $title) !== false; } ); } public static function titleMatches($titleRegexp) { return new static( function (WebDriver $driver) use ($titleRegexp) { return (bool) preg_match($titleRegexp, $driver->getTitle()); } ); } public static function urlIs($url) { return new static( function (WebDriver $driver) use ($url) { return $url === $driver->getCurrentURL(); } ); } public static function urlContains($url) { return new static( function (WebDriver $driver) use ($url) { return mb_strpos($driver->getCurrentURL(), $url) !== false; } ); } public static function urlMatches($urlRegexp) { return new static( function (WebDriver $driver) use ($urlRegexp) { return (bool) preg_match($urlRegexp, $driver->getCurrentURL()); } ); } public static function presenceOfElementLocated(WebDriverBy $by) { return new static( function (WebDriver $driver) use ($by) { return $driver->findElement($by); } ); } public static function presenceOfAllElementsLocatedBy(WebDriverBy $by) { return new static( function (WebDriver $driver) use ($by) { $elements = $driver->findElements($by); return count($elements) > 0 ? $elements : null; } ); } public static function visibilityOfElementLocated(WebDriverBy $by) { return new static( function (WebDriver $driver) use ($by) { try { $element = $driver->findElement($by); return $element->isDisplayed() ? $element : null; } catch (StaleElementReferenceException $e) { return null; } } ); } public static function visibilityOfAnyElementLocated(WebDriverBy $by) { return new static( function (WebDriver $driver) use ($by) { $elements = $driver->findElements($by); $visibleElements = []; foreach ($elements as $element) { try { if ($element->isDisplayed()) { $visibleElements[] = $element; } } catch (StaleElementReferenceException $e) { } } return count($visibleElements) > 0 ? $visibleElements : null; } ); } public static function visibilityOf(WebDriverElement $element) { return new static( function () use ($element) { return $element->isDisplayed() ? $element : null; } ); } public static function textToBePresentInElement(WebDriverBy $by, $text) { return self::elementTextContains($by, $text); } public static function elementTextContains(WebDriverBy $by, $text) { return new static( function (WebDriver $driver) use ($by, $text) { try { $element_text = $driver->findElement($by)->getText(); return mb_strpos($element_text, $text) !== false; } catch (StaleElementReferenceException $e) { return null; } } ); } public static function elementTextIs(WebDriverBy $by, $text) { return new static( function (WebDriver $driver) use ($by, $text) { try { return $driver->findElement($by)->getText() == $text; } catch (StaleElementReferenceException $e) { return null; } } ); } public static function elementTextMatches(WebDriverBy $by, $regexp) { return new static( function (WebDriver $driver) use ($by, $regexp) { try { return (bool) preg_match($regexp, $driver->findElement($by)->getText()); } catch (StaleElementReferenceException $e) { return null; } } ); } public static function textToBePresentInElementValue(WebDriverBy $by, $text) { return self::elementValueContains($by, $text); } public static function elementValueContains(WebDriverBy $by, $text) { return new static( function (WebDriver $driver) use ($by, $text) { try { $element_text = $driver->findElement($by)->getAttribute('value'); return mb_strpos($element_text, $text) !== false; } catch (StaleElementReferenceException $e) { return null; } } ); } public static function frameToBeAvailableAndSwitchToIt($frame_locator) { return new static( function (WebDriver $driver) use ($frame_locator) { try { return $driver->switchTo()->frame($frame_locator); } catch (NoSuchFrameException $e) { return false; } } ); } public static function invisibilityOfElementLocated(WebDriverBy $by) { return new static( function (WebDriver $driver) use ($by) { try { return !$driver->findElement($by)->isDisplayed(); } catch (NoSuchElementException $e) { return true; } catch (StaleElementReferenceException $e) { return true; } } ); } public static function invisibilityOfElementWithText(WebDriverBy $by, $text) { return new static( function (WebDriver $driver) use ($by, $text) { try { return !($driver->findElement($by)->getText() === $text); } catch (NoSuchElementException $e) { return true; } catch (StaleElementReferenceException $e) { return true; } } ); } public static function elementToBeClickable(WebDriverBy $by) { $visibility_of_element_located = self::visibilityOfElementLocated($by); return new static( function (WebDriver $driver) use ($visibility_of_element_located) { $element = call_user_func( $visibility_of_element_located->getApply(), $driver ); try { if ($element !== null && $element->isEnabled()) { return $element; } return null; } catch (StaleElementReferenceException $e) { return null; } } ); } public static function stalenessOf(WebDriverElement $element) { return new static( function () use ($element) { try { $element->isEnabled(); return false; } catch (StaleElementReferenceException $e) { return true; } } ); } public static function refreshed(self $condition) { return new static( function (WebDriver $driver) use ($condition) { try { return call_user_func($condition->getApply(), $driver); } catch (StaleElementReferenceException $e) { return null; } } ); } public static function elementToBeSelected($element_or_by) { return self::elementSelectionStateToBe( $element_or_by, true ); } public static function elementSelectionStateToBe($element_or_by, $selected) { if ($element_or_by instanceof WebDriverElement) { return new static( function () use ($element_or_by, $selected) { return $element_or_by->isSelected() === $selected; } ); } else { if ($element_or_by instanceof WebDriverBy) { return new static( function (WebDriver $driver) use ($element_or_by, $selected) { try { $element = $driver->findElement($element_or_by); return $element->isSelected() === $selected; } catch (StaleElementReferenceException $e) { return null; } } ); } } } public static function alertIsPresent() { return new static( function (WebDriver $driver) { try { $alert = $driver->switchTo()->alert(); $alert->getText(); return $alert; } catch (NoAlertOpenException $e) { return null; } } ); } public static function numberOfWindowsToBe($expectedNumberOfWindows) { return new static( function (WebDriver $driver) use ($expectedNumberOfWindows) { return count($driver->getWindowHandles()) == $expectedNumberOfWindows; } ); } public static function not(self $condition) { return new static( function (WebDriver $driver) use ($condition) { $result = call_user_func($condition->getApply(), $driver); return !$result; } ); } } executor = $executor; } public function back() { $this->executor->execute(DriverCommand::GO_BACK); return $this; } public function forward() { $this->executor->execute(DriverCommand::GO_FORWARD); return $this; } public function refresh() { $this->executor->execute(DriverCommand::REFRESH); return $this; } public function to($url) { $params = ['url' => (string) $url]; $this->executor->execute(DriverCommand::GET, $params); return $this; } } executor = $executor; } public function addCookie($cookie) { if (is_array($cookie)) { $cookie = Cookie::createFromArray($cookie); } if (!$cookie instanceof Cookie) { throw new InvalidArgumentException('Cookie must be set from instance of Cookie class or from array.'); } $this->executor->execute( DriverCommand::ADD_COOKIE, ['cookie' => $cookie->toArray()] ); return $this; } public function deleteAllCookies() { $this->executor->execute(DriverCommand::DELETE_ALL_COOKIES); return $this; } public function deleteCookieNamed($name) { $this->executor->execute( DriverCommand::DELETE_COOKIE, [':name' => $name] ); return $this; } public function getCookieNamed($name) { $cookies = $this->getCookies(); foreach ($cookies as $cookie) { if ($cookie['name'] === $name) { return $cookie; } } return null; } public function getCookies() { $cookieArrays = $this->executor->execute(DriverCommand::GET_ALL_COOKIES); $cookies = []; foreach ($cookieArrays as $cookieArray) { $cookies[] = Cookie::createFromArray($cookieArray); } return $cookies; } public function timeouts() { return new WebDriverTimeouts($this->executor); } public function window() { return new WebDriverWindow($this->executor); } public function getLog($log_type) { return $this->executor->execute( DriverCommand::GET_LOG, ['type' => $log_type] ); } public function getAvailableLogTypes() { return $this->executor->execute(DriverCommand::GET_AVAILABLE_LOG_TYPES); } } x = $x; $this->y = $y; } public function getX() { return $this->x; } public function getY() { return $this->y; } public function move($new_x, $new_y) { $this->x = $new_x; $this->y = $new_y; return $this; } public function moveBy($x_offset, $y_offset) { $this->x += $x_offset; $this->y += $y_offset; return $this; } public function equals(self $point) { return $this->x === $point->getX() && $this->y === $point->getY(); } } type = $element->getAttribute('type'); if ($this->type !== 'radio') { throw new WebDriverException('The input must be of type "radio".'); } } public function isMultiple() { return false; } public function deselectAll() { throw new UnsupportedOperationException('You cannot deselect radio buttons'); } public function deselectByIndex($index) { throw new UnsupportedOperationException('You cannot deselect radio buttons'); } public function deselectByValue($value) { throw new UnsupportedOperationException('You cannot deselect radio buttons'); } public function deselectByVisibleText($text) { throw new UnsupportedOperationException('You cannot deselect radio buttons'); } public function deselectByVisiblePartialText($text) { throw new UnsupportedOperationException('You cannot deselect radio buttons'); } } getTagName(); if ($tag_name !== 'select') { throw new UnexpectedTagNameException('select', $tag_name); } $this->element = $element; $value = $element->getAttribute('multiple'); $this->isMulti = $value === 'true'; } public function isMultiple() { return $this->isMulti; } public function getOptions() { return $this->element->findElements(WebDriverBy::tagName('option')); } public function getAllSelectedOptions() { $selected_options = []; foreach ($this->getOptions() as $option) { if ($option->isSelected()) { $selected_options[] = $option; if (!$this->isMultiple()) { return $selected_options; } } } return $selected_options; } public function getFirstSelectedOption() { foreach ($this->getOptions() as $option) { if ($option->isSelected()) { return $option; } } throw new NoSuchElementException('No options are selected'); } public function selectByIndex($index) { foreach ($this->getOptions() as $option) { if ($option->getAttribute('index') === (string) $index) { $this->selectOption($option); return; } } throw new NoSuchElementException(sprintf('Cannot locate option with index: %d', $index)); } public function selectByValue($value) { $matched = false; $xpath = './/option[@value = ' . XPathEscaper::escapeQuotes($value) . ']'; $options = $this->element->findElements(WebDriverBy::xpath($xpath)); foreach ($options as $option) { $this->selectOption($option); if (!$this->isMultiple()) { return; } $matched = true; } if (!$matched) { throw new NoSuchElementException( sprintf('Cannot locate option with value: %s', $value) ); } } public function selectByVisibleText($text) { $matched = false; $xpath = './/option[normalize-space(.) = ' . XPathEscaper::escapeQuotes($text) . ']'; $options = $this->element->findElements(WebDriverBy::xpath($xpath)); foreach ($options as $option) { $this->selectOption($option); if (!$this->isMultiple()) { return; } $matched = true; } if (!$matched) { foreach ($this->getOptions() as $option) { if ($option->getText() === $text) { $this->selectOption($option); if (!$this->isMultiple()) { return; } $matched = true; } } } if (!$matched) { throw new NoSuchElementException( sprintf('Cannot locate option with text: %s', $text) ); } } public function selectByVisiblePartialText($text) { $matched = false; $xpath = './/option[contains(normalize-space(.), ' . XPathEscaper::escapeQuotes($text) . ')]'; $options = $this->element->findElements(WebDriverBy::xpath($xpath)); foreach ($options as $option) { $this->selectOption($option); if (!$this->isMultiple()) { return; } $matched = true; } if (!$matched) { throw new NoSuchElementException( sprintf('Cannot locate option with text: %s', $text) ); } } public function deselectAll() { if (!$this->isMultiple()) { throw new UnsupportedOperationException('You may only deselect all options of a multi-select'); } foreach ($this->getOptions() as $option) { $this->deselectOption($option); } } public function deselectByIndex($index) { if (!$this->isMultiple()) { throw new UnsupportedOperationException('You may only deselect options of a multi-select'); } foreach ($this->getOptions() as $option) { if ($option->getAttribute('index') === (string) $index) { $this->deselectOption($option); return; } } } public function deselectByValue($value) { if (!$this->isMultiple()) { throw new UnsupportedOperationException('You may only deselect options of a multi-select'); } $xpath = './/option[@value = ' . XPathEscaper::escapeQuotes($value) . ']'; $options = $this->element->findElements(WebDriverBy::xpath($xpath)); foreach ($options as $option) { $this->deselectOption($option); } } public function deselectByVisibleText($text) { if (!$this->isMultiple()) { throw new UnsupportedOperationException('You may only deselect options of a multi-select'); } $xpath = './/option[normalize-space(.) = ' . XPathEscaper::escapeQuotes($text) . ']'; $options = $this->element->findElements(WebDriverBy::xpath($xpath)); foreach ($options as $option) { $this->deselectOption($option); } } public function deselectByVisiblePartialText($text) { if (!$this->isMultiple()) { throw new UnsupportedOperationException('You may only deselect options of a multi-select'); } $xpath = './/option[contains(normalize-space(.), ' . XPathEscaper::escapeQuotes($text) . ')]'; $options = $this->element->findElements(WebDriverBy::xpath($xpath)); foreach ($options as $option) { $this->deselectOption($option); } } protected function selectOption(WebDriverElement $option) { if (!$option->isSelected()) { $option->click(); } } protected function deselectOption(WebDriverElement $option) { if ($option->isSelected()) { $option->click(); } } } executor = $executor; } public function implicitlyWait($seconds) { $this->executor->execute( DriverCommand::IMPLICITLY_WAIT, ['ms' => $seconds * 1000] ); return $this; } public function setScriptTimeout($seconds) { $this->executor->execute( DriverCommand::SET_SCRIPT_TIMEOUT, ['ms' => $seconds * 1000] ); return $this; } public function pageLoadTimeout($seconds) { $this->executor->execute(DriverCommand::SET_TIMEOUT, [ 'type' => 'page load', 'ms' => $seconds * 1000, ]); return $this; } } x = $x; $this->y = $y; parent::__construct($touch_screen); } public function perform() { $this->touchScreen->up($this->x, $this->y); } } driver = $driver; $this->timeout = isset($timeout_in_second) ? $timeout_in_second : 30; $this->interval = $interval_in_millisecond ?: 250; } public function until($func_or_ec, $message = '') { $end = microtime(true) + $this->timeout; $last_exception = null; while ($end > microtime(true)) { try { if ($func_or_ec instanceof WebDriverExpectedCondition) { $ret_val = call_user_func($func_or_ec->getApply(), $this->driver); } else { $ret_val = call_user_func($func_or_ec, $this->driver); } if ($ret_val) { return $ret_val; } } catch (NoSuchElementException $e) { $last_exception = $e; } usleep($this->interval * 1000); } if ($last_exception) { throw $last_exception; } throw new TimeOutException($message); } } executor = $executor; } public function getPosition() { $position = $this->executor->execute( DriverCommand::GET_WINDOW_POSITION, [':windowHandle' => 'current'] ); return new WebDriverPoint( $position['x'], $position['y'] ); } public function getSize() { $size = $this->executor->execute( DriverCommand::GET_WINDOW_SIZE, [':windowHandle' => 'current'] ); return new WebDriverDimension( $size['width'], $size['height'] ); } public function maximize() { $this->executor->execute( DriverCommand::MAXIMIZE_WINDOW, [':windowHandle' => 'current'] ); return $this; } public function setSize(WebDriverDimension $size) { $params = [ 'width' => $size->getWidth(), 'height' => $size->getHeight(), ':windowHandle' => 'current', ]; $this->executor->execute(DriverCommand::SET_WINDOW_SIZE, $params); return $this; } public function setPosition(WebDriverPoint $position) { $params = [ 'x' => $position->getX(), 'y' => $position->getY(), ':windowHandle' => 'current', ]; $this->executor->execute(DriverCommand::SET_WINDOW_POSITION, $params); return $this; } public function getScreenOrientation() { return $this->executor->execute(DriverCommand::GET_SCREEN_ORIENTATION); } public function setScreenOrientation($orientation) { $orientation = mb_strtoupper($orientation); if (!in_array($orientation, ['PORTRAIT', 'LANDSCAPE'])) { throw new IndexOutOfBoundsException( 'Orientation must be either PORTRAIT, or LANDSCAPE' ); } $this->executor->execute( DriverCommand::SET_SCREEN_ORIENTATION, ['orientation' => $orientation] ); return $this; } } __get($key); } if (is_object($collection) && !$collection instanceof \ArrayAccess) { return $collection->$key; } if (is_array($collection)) { if (is_int($key)) { return array_slice($collection, $key, 1, false)[0]; } else { return $collection[$key]; } } if (is_object($collection) && !$collection instanceof \ArrayAccess) { return $collection->$key; } if (is_int($key)) { $i = 0; foreach ($collection as $val) { if ($i === $key) { return $val; } $i += 1; } if ($key < 0) { $total = $i; $i = 0; foreach ($collection as $val) { if ($i - $total === $key) { return $val; } $i += 1; } } } return $collection[$key]; } public static function setValue(&$collection, $key, $value) { if (is_object($collection) && ! $collection instanceof \ArrayAccess) { return $collection->$key = $value; } else { return $collection[$key] = $value; } } public static function unsetValue(&$collection, $key) { if (is_object($collection) && ! $collection instanceof \ArrayAccess) { unset($collection->$key); } else { unset($collection[$key]); } } public static function arrayValues($collection) { if (is_array($collection)) { return array_values($collection); } else if (is_object($collection)) { return array_values((array) $collection); } throw new JSONPathException("Invalid variable type for arrayValues"); } } token = $token; $this->options = $options; $this->magicIsAllowed = $this->options & JSONPath::ALLOW_MAGIC; } public function isMagicAllowed() { return $this->magicIsAllowed; } abstract public function filter($collection); } token->value as $index) { if (AccessHelper::keyExists($collection, $index, $this->magicIsAllowed)) { $return[] = AccessHelper::getValue($collection, $index, $this->magicIsAllowed); } } return $return; } } token->value, $this->magicIsAllowed)) { return [ AccessHelper::getValue($collection, $this->token->value, $this->magicIsAllowed) ]; } else if ($this->token->value === "*") { return AccessHelper::arrayValues($collection); } return []; } } \w+)|\[["\']?(?.*?)["\']?\]) (\s*(?==|=|<>|!==|!=|>|<)\s*(?.+))? '; public function filter($collection) { $return = []; preg_match('/^' . static::MATCH_QUERY_OPERATORS . '$/x', $this->token->value, $matches); if (!isset($matches[1])) { throw new \Exception("Malformed filter query"); } $key = $matches['key'] ?: $matches['keySquare']; if ($key === "") { throw new \Exception("Malformed filter query: key was not set"); } $operator = isset($matches['operator']) ? $matches['operator'] : null; $comparisonValue = isset($matches['comparisonValue']) ? $matches['comparisonValue'] : null; if (strtolower($comparisonValue) === "false") { $comparisonValue = false; } if (strtolower($comparisonValue) === "true") { $comparisonValue = true; } if (strtolower($comparisonValue) === "null") { $comparisonValue = null; } $comparisonValue = preg_replace('/^[\'"]/', '', $comparisonValue); $comparisonValue = preg_replace('/[\'"]$/', '', $comparisonValue); foreach ($collection as $value) { if (AccessHelper::keyExists($value, $key, $this->magicIsAllowed)) { $value1 = AccessHelper::getValue($value, $key, $this->magicIsAllowed); if ($operator === null && AccessHelper::keyExists($value, $key, $this->magicIsAllowed)) { $return[] = $value; } if (($operator === "=" || $operator === "==") && $value1 == $comparisonValue) { $return[] = $value; } if (($operator === "!=" || $operator === "!==" || $operator === "<>") && $value1 != $comparisonValue) { $return[] = $value; } if ($operator == ">" && $value1 > $comparisonValue) { $return[] = $value; } if ($operator == "<" && $value1 < $comparisonValue) { $return[] = $value; } } } return $return; } } \w+)\s*(?-|\+|\*|\/)\s*(?\d+)/', $this->token->value, $matches); $matchKey = $matches['key']; if (AccessHelper::keyExists($collection, $matchKey, $this->magicIsAllowed)) { $value = AccessHelper::getValue($collection, $matchKey, $this->magicIsAllowed); } else { if ($matches['key'] === 'length') { $value = count($collection); } else { return; } } switch ($matches['operator']) { case '+': $resultKey = $value + $matches['numeric']; break; case '*': $resultKey = $value * $matches['numeric']; break; case '-': $resultKey = $value - $matches['numeric']; break; case '/': $resultKey = $value / $matches['numeric']; break; default: throw new JSONPathException("Unsupported operator in expression"); break; } if (AccessHelper::keyExists($collection, $resultKey, $this->magicIsAllowed)) { $result[] = AccessHelper::getValue($collection, $resultKey, $this->magicIsAllowed); } return $result; } } recurse($result, $collection); return $result; } private function recurse(& $result, $data) { $result[] = $data; if (AccessHelper::isCollectionType($data)) { foreach (AccessHelper::arrayValues($data) as $key => $value) { $results[] = $value; if (AccessHelper::isCollectionType($value)) { $this->recurse($result, $value); } } } } } token->value['start']; $end = $this->token->value['end']; $step = $this->token->value['step'] ?: 1; if ($start !== null && $end !== null) { } if ($start !== null && $end === null) { $end = $start < 0 ? -1 : $length - 1; } if ($start === null && $end !== null) { $start = 0; } if ($start === null && $end === null) { $start = 0; $end = $length - 1; } if ($end < 0 && $start >= 0) { $end = ($length + $end) - 1; } if ($start < 0 && $end === null) { $end = -1; } for ($i = $start; $i <= $end; $i += $step) { $index = $i; if ($i < 0) { $index = $length + $i; } if (AccessHelper::keyExists($collection, $index, $this->magicIsAllowed)) { $result[] = $collection[$index]; } } return $result; } } data = $data; $this->options = $options; } public function find($expression) { $tokens = $this->parseTokens($expression); $collectionData = [$this->data]; foreach ($tokens as $token) { $filter = $token->buildFilter($this->options); $filteredData = []; foreach ($collectionData as $value) { if (AccessHelper::isCollectionType($value)) { $filteredValue = $filter->filter($value); $filteredData = array_merge($filteredData, $filteredValue); } } $collectionData = $filteredData; } return new static($collectionData, $this->options); } public function first() { $keys = AccessHelper::collectionKeys($this->data); if (empty($keys)) { return null; } $value = isset($this->data[$keys[0]]) ? $this->data[$keys[0]] : null; return AccessHelper::isCollectionType($value) ? new static($value, $this->options) : $value; } public function last() { $keys = AccessHelper::collectionKeys($this->data); if (empty($keys)) { return null; } $value = $this->data[end($keys)] ? $this->data[end($keys)] : null; return AccessHelper::isCollectionType($value) ? new static($value, $this->options) : $value; } public function firstKey() { $keys = AccessHelper::collectionKeys($this->data); if (empty($keys)) { return null; } return $keys[0]; } public function lastKey() { $keys = AccessHelper::collectionKeys($this->data); if (empty($keys) || end($keys) === false) { return null; } return end($keys); } public function parseTokens($expression) { $cacheKey = md5($expression); if (isset(static::$tokenCache[$cacheKey])) { return static::$tokenCache[$cacheKey]; } $lexer = new JSONPathLexer($expression); $tokens = $lexer->parseExpression(); static::$tokenCache[$cacheKey] = $tokens; return $tokens; } public function data() { return $this->data; } public function offsetExists($offset) { return AccessHelper::keyExists($this->data, $offset); } public function offsetGet($offset) { $value = AccessHelper::getValue($this->data, $offset); return AccessHelper::isCollectionType($value) ? new static($value, $this->options) : $value; } public function offsetSet($offset, $value) { if ($offset === null) { $this->data[] = $value; } else { AccessHelper::setValue($this->data, $offset, $value); } } public function offsetUnset($offset) { AccessHelper::unsetValue($this->data, $offset); } public function jsonSerialize() { return $this->data; } public function current() { $value = current($this->data); return AccessHelper::isCollectionType($value) ? new static($value, $this->options) : $value; } public function next() { next($this->data); } public function key() { return key($this->data); } public function valid() { return key($this->data) !== null; } public function rewind() { reset($this->data); } public function __get($key) { return $this->offsetExists($key) ? $this->offsetGet($key) : null; } public function count() { return count($this->data); } } expression = $expression; $this->expressionLength = strlen($expression); } public function parseExpressionTokens() { $dotIndexDepth = 0; $squareBracketDepth = 0; $capturing = false; $tokenValue = ''; $tokens = []; for ($i = 0; $i < $this->expressionLength; $i++) { $char = $this->expression[$i]; if ($squareBracketDepth === 0) { if ($char === '.') { if ($this->lookAhead($i, 1) === ".") { $tokens[] = new JSONPathToken(JSONPathToken::T_RECURSIVE, null); } continue; } } if ($char === '[') { $squareBracketDepth += 1; if ($squareBracketDepth === 1) { continue; } } if ($char === ']') { $squareBracketDepth -= 1; if ($squareBracketDepth === 0) { continue; } } if ($squareBracketDepth > 0) { $tokenValue .= $char; if ($this->lookAhead($i, 1) === ']' && $squareBracketDepth === 1) { $tokens[] = $this->createToken($tokenValue); $tokenValue = ''; } } if ($squareBracketDepth === 0) { $tokenValue .= $char; if ($char === "." && $dotIndexDepth > 1) { $tokens[] = $this->createToken($tokenValue); $tokenValue = ''; continue; } if ($this->lookAhead($i, 1) === '.' || $this->lookAhead($i, 1) === '[' || $this->atEnd($i)) { $tokens[] = $this->createToken($tokenValue); $tokenValue = ''; $dotIndexDepth -= 1; } } } if ($tokenValue !== '') { $tokens[] = $this->createToken($tokenValue); $tokenValue = ''; } return $tokens; } protected function lookAhead($pos, $forward = 1) { return isset($this->expression[$pos + $forward]) ? $this->expression[$pos + $forward] : null; } protected function atEnd($pos) { return $pos === $this->expressionLength; } public function parseExpression() { $tokens = $this->parseExpressionTokens(); return $tokens; } protected function createToken($value) { if (preg_match('/^(' . static::MATCH_INDEX . ')$/x', $value, $matches)) { if (preg_match('/^-?\d+$/', $value)) { $value = (int)$value; } return new JSONPathToken(JSONPathToken::T_INDEX, $value); } if (preg_match('/^' . static::MATCH_INDEXES . '$/x', $value, $matches)) { $value = explode(',', trim($value, ',')); foreach ($value as $i => $v) { $value[$i] = (int) trim($v); } return new JSONPathToken(JSONPathToken::T_INDEXES, $value); } if (preg_match('/^' . static::MATCH_SLICE . '$/x', $value, $matches)) { $parts = explode(':', $value); $value = [ 'start' => isset($parts[0]) && $parts[0] !== "" ? (int) $parts[0] : null, 'end' => isset($parts[1]) && $parts[1] !== "" ? (int) $parts[1] : null, 'step' => isset($parts[2]) && $parts[2] !== "" ? (int) $parts[2] : null, ]; return new JSONPathToken(JSONPathToken::T_SLICE, $value); } if (preg_match('/^' . static::MATCH_QUERY_RESULT . '$/x', $value)) { $value = substr($value, 1, -1); return new JSONPathToken(JSONPathToken::T_QUERY_RESULT, $value); } if (preg_match('/^' . static::MATCH_QUERY_MATCH . '$/x', $value)) { $value = substr($value, 2, -1); return new JSONPathToken(JSONPathToken::T_QUERY_MATCH, $value); } if (preg_match('/^' . static::MATCH_INDEX_IN_SINGLE_QUOTES . '$/x', $value, $matches)) { $value = $matches[1]; $value = trim($value); return new JSONPathToken(JSONPathToken::T_INDEX, $value); } if (preg_match('/^' . static::MATCH_INDEX_IN_DOUBLE_QUOTES . '$/x', $value, $matches)) { $value = $matches[1]; $value = trim($value); return new JSONPathToken(JSONPathToken::T_INDEX, $value); } throw new JSONPathException("Unable to parse token {$value} in expression: $this->expression"); } } validateType($type); $this->type = $type; $this->value = $value; } public function validateType($type) { if (!in_array($type, static::getTypes(), true)) { throw new JSONPathException('Invalid token: ' . $type); } } public static function getTypes() { return [ static::T_INDEX, static::T_RECURSIVE, static::T_QUERY_RESULT, static::T_QUERY_MATCH, static::T_SLICE, static::T_INDEXES, ]; } public function buildFilter($options) { $filterClass = 'Flow\\JSONPath\\Filters\\' . ucfirst($this->type) . 'Filter'; if (! class_exists($filterClass)) { throw new JSONPathException("No filter class exists for token [{$this->type}]"); } return new $filterClass($this, $options); } }configureDefaults($config); } public function __call($method, $args) { if (count($args) < 1) { throw new \InvalidArgumentException('Magic request methods require a URI and optional options array'); } $uri = $args[0]; $opts = isset($args[1]) ? $args[1] : []; return substr($method, -5) === 'Async' ? $this->requestAsync(substr($method, 0, -5), $uri, $opts) : $this->request($method, $uri, $opts); } public function sendAsync(RequestInterface $request, array $options = []) { $options = $this->prepareDefaults($options); return $this->transfer( $request->withUri($this->buildUri($request->getUri(), $options), $request->hasHeader('Host')), $options ); } public function send(RequestInterface $request, array $options = []) { $options[RequestOptions::SYNCHRONOUS] = true; return $this->sendAsync($request, $options)->wait(); } public function requestAsync($method, $uri = '', array $options = []) { $options = $this->prepareDefaults($options); $headers = isset($options['headers']) ? $options['headers'] : []; $body = isset($options['body']) ? $options['body'] : null; $version = isset($options['version']) ? $options['version'] : '1.1'; $uri = $this->buildUri($uri, $options); if (is_array($body)) { $this->invalidBody(); } $request = new Psr7\Request($method, $uri, $headers, $body, $version); unset($options['headers'], $options['body'], $options['version']); return $this->transfer($request, $options); } public function request($method, $uri = '', array $options = []) { $options[RequestOptions::SYNCHRONOUS] = true; return $this->requestAsync($method, $uri, $options)->wait(); } public function getConfig($option = null) { return $option === null ? $this->config : (isset($this->config[$option]) ? $this->config[$option] : null); } private function buildUri($uri, array $config) { $uri = Psr7\uri_for($uri === null ? '' : $uri); if (isset($config['base_uri'])) { $uri = Psr7\UriResolver::resolve(Psr7\uri_for($config['base_uri']), $uri); } return $uri->getScheme() === '' && $uri->getHost() !== '' ? $uri->withScheme('http') : $uri; } private function configureDefaults(array $config) { $defaults = [ 'allow_redirects' => RedirectMiddleware::$defaultSettings, 'http_errors' => true, 'decode_content' => true, 'verify' => true, 'cookies' => false ]; if (php_sapi_name() == 'cli' && getenv('HTTP_PROXY')) { $defaults['proxy']['http'] = getenv('HTTP_PROXY'); } if ($proxy = getenv('HTTPS_PROXY')) { $defaults['proxy']['https'] = $proxy; } if ($noProxy = getenv('NO_PROXY')) { $cleanedNoProxy = str_replace(' ', '', $noProxy); $defaults['proxy']['no'] = explode(',', $cleanedNoProxy); } $this->config = $config + $defaults; if (!empty($config['cookies']) && $config['cookies'] === true) { $this->config['cookies'] = new CookieJar(); } if (!isset($this->config['headers'])) { $this->config['headers'] = ['User-Agent' => default_user_agent()]; } else { foreach (array_keys($this->config['headers']) as $name) { if (strtolower($name) === 'user-agent') { return; } } $this->config['headers']['User-Agent'] = default_user_agent(); } } private function prepareDefaults($options) { $defaults = $this->config; if (!empty($defaults['headers'])) { $defaults['_conditional'] = $defaults['headers']; unset($defaults['headers']); } if (array_key_exists('headers', $options)) { if ($options['headers'] === null) { $defaults['_conditional'] = null; unset($options['headers']); } elseif (!is_array($options['headers'])) { throw new \InvalidArgumentException('headers must be an array'); } } $result = $options + $defaults; foreach ($result as $k => $v) { if ($v === null) { unset($result[$k]); } } return $result; } private function transfer(RequestInterface $request, array $options) { if (isset($options['save_to'])) { $options['sink'] = $options['save_to']; unset($options['save_to']); } if (isset($options['exceptions'])) { $options['http_errors'] = $options['exceptions']; unset($options['exceptions']); } $request = $this->applyOptions($request, $options); $handler = $options['handler']; try { return Promise\promise_for($handler($request, $options)); } catch (\Exception $e) { return Promise\rejection_for($e); } } private function applyOptions(RequestInterface $request, array &$options) { $modify = [ 'set_headers' => [], ]; if (isset($options['headers'])) { $modify['set_headers'] = $options['headers']; unset($options['headers']); } if (isset($options['form_params'])) { if (isset($options['multipart'])) { throw new \InvalidArgumentException('You cannot use ' . 'form_params and multipart at the same time. Use the ' . 'form_params option if you want to send application/' . 'x-www-form-urlencoded requests, and the multipart ' . 'option to send multipart/form-data requests.'); } $options['body'] = http_build_query($options['form_params'], '', '&'); unset($options['form_params']); $options['_conditional'] = Psr7\_caseless_remove(['Content-Type'], $options['_conditional']); $options['_conditional']['Content-Type'] = 'application/x-www-form-urlencoded'; } if (isset($options['multipart'])) { $options['body'] = new Psr7\MultipartStream($options['multipart']); unset($options['multipart']); } if (isset($options['json'])) { $options['body'] = \GuzzleHttp\json_encode($options['json']); unset($options['json']); $options['_conditional'] = Psr7\_caseless_remove(['Content-Type'], $options['_conditional']); $options['_conditional']['Content-Type'] = 'application/json'; } if (!empty($options['decode_content']) && $options['decode_content'] !== true ) { $options['_conditional'] = Psr7\_caseless_remove(['Accept-Encoding'], $options['_conditional']); $modify['set_headers']['Accept-Encoding'] = $options['decode_content']; } if (isset($options['body'])) { if (is_array($options['body'])) { $this->invalidBody(); } $modify['body'] = Psr7\stream_for($options['body']); unset($options['body']); } if (!empty($options['auth']) && is_array($options['auth'])) { $value = $options['auth']; $type = isset($value[2]) ? strtolower($value[2]) : 'basic'; switch ($type) { case 'basic': $modify['set_headers'] = Psr7\_caseless_remove(['Authorization'], $modify['set_headers']); $modify['set_headers']['Authorization'] = 'Basic ' . base64_encode("$value[0]:$value[1]"); break; case 'digest': $options['curl'][CURLOPT_HTTPAUTH] = CURLAUTH_DIGEST; $options['curl'][CURLOPT_USERPWD] = "$value[0]:$value[1]"; break; case 'ntlm': $options['curl'][CURLOPT_HTTPAUTH] = CURLAUTH_NTLM; $options['curl'][CURLOPT_USERPWD] = "$value[0]:$value[1]"; break; } } if (isset($options['query'])) { $value = $options['query']; if (is_array($value)) { $value = http_build_query($value, null, '&', PHP_QUERY_RFC3986); } if (!is_string($value)) { throw new \InvalidArgumentException('query must be a string or array'); } $modify['query'] = $value; unset($options['query']); } if (isset($options['sink'])) { if (is_bool($options['sink'])) { throw new \InvalidArgumentException('sink must not be a boolean'); } } $request = Psr7\modify_request($request, $modify); if ($request->getBody() instanceof Psr7\MultipartStream) { $options['_conditional'] = Psr7\_caseless_remove(['Content-Type'], $options['_conditional']); $options['_conditional']['Content-Type'] = 'multipart/form-data; boundary=' . $request->getBody()->getBoundary(); } if (isset($options['_conditional'])) { $modify = []; foreach ($options['_conditional'] as $k => $v) { if (!$request->hasHeader($k)) { $modify['set_headers'][$k] = $v; } } $request = Psr7\modify_request($request, $modify); unset($options['_conditional']); } return $request; } private function invalidBody() { throw new \InvalidArgumentException('Passing in the "body" request ' . 'option as an array to send a POST request has been deprecated. ' . 'Please use the "form_params" request option to send a ' . 'application/x-www-form-urlencoded request, or the "multipart" ' . 'request option to send a multipart/form-data request.'); } } strictMode = $strictMode; foreach ($cookieArray as $cookie) { if (!($cookie instanceof SetCookie)) { $cookie = new SetCookie($cookie); } $this->setCookie($cookie); } } public static function fromArray(array $cookies, $domain) { $cookieJar = new self(); foreach ($cookies as $name => $value) { $cookieJar->setCookie(new SetCookie([ 'Domain' => $domain, 'Name' => $name, 'Value' => $value, 'Discard' => true ])); } return $cookieJar; } public static function getCookieValue($value) { return $value; } public static function shouldPersist( SetCookie $cookie, $allowSessionCookies = false ) { if ($cookie->getExpires() || $allowSessionCookies) { if (!$cookie->getDiscard()) { return true; } } return false; } public function getCookieByName($name) { if ($name === null) { return null; } foreach ($this->cookies as $cookie) { if ($cookie->getName() !== null && strcasecmp($cookie->getName(), $name) === 0) { return $cookie; } } } public function toArray() { return array_map(function (SetCookie $cookie) { return $cookie->toArray(); }, $this->getIterator()->getArrayCopy()); } public function clear($domain = null, $path = null, $name = null) { if (!$domain) { $this->cookies = []; return; } elseif (!$path) { $this->cookies = array_filter( $this->cookies, function (SetCookie $cookie) use ($path, $domain) { return !$cookie->matchesDomain($domain); } ); } elseif (!$name) { $this->cookies = array_filter( $this->cookies, function (SetCookie $cookie) use ($path, $domain) { return !($cookie->matchesPath($path) && $cookie->matchesDomain($domain)); } ); } else { $this->cookies = array_filter( $this->cookies, function (SetCookie $cookie) use ($path, $domain, $name) { return !($cookie->getName() == $name && $cookie->matchesPath($path) && $cookie->matchesDomain($domain)); } ); } } public function clearSessionCookies() { $this->cookies = array_filter( $this->cookies, function (SetCookie $cookie) { return !$cookie->getDiscard() && $cookie->getExpires(); } ); } public function setCookie(SetCookie $cookie) { $name = $cookie->getName(); if (!$name && $name !== '0') { return false; } $result = $cookie->validate(); if ($result !== true) { if ($this->strictMode) { throw new \RuntimeException('Invalid cookie: ' . $result); } else { $this->removeCookieIfEmpty($cookie); return false; } } foreach ($this->cookies as $i => $c) { if ($c->getPath() != $cookie->getPath() || $c->getDomain() != $cookie->getDomain() || $c->getName() != $cookie->getName() ) { continue; } if (!$cookie->getDiscard() && $c->getDiscard()) { unset($this->cookies[$i]); continue; } if ($cookie->getExpires() > $c->getExpires()) { unset($this->cookies[$i]); continue; } if ($cookie->getValue() !== $c->getValue()) { unset($this->cookies[$i]); continue; } return false; } $this->cookies[] = $cookie; return true; } public function count() { return count($this->cookies); } public function getIterator() { return new \ArrayIterator(array_values($this->cookies)); } public function extractCookies( RequestInterface $request, ResponseInterface $response ) { if ($cookieHeader = $response->getHeader('Set-Cookie')) { foreach ($cookieHeader as $cookie) { $sc = SetCookie::fromString($cookie); if (!$sc->getDomain()) { $sc->setDomain($request->getUri()->getHost()); } if (0 !== strpos($sc->getPath(), '/')) { $sc->setPath($this->getCookiePathFromRequest($request)); } $this->setCookie($sc); } } } private function getCookiePathFromRequest(RequestInterface $request) { $uriPath = $request->getUri()->getPath(); if ('' === $uriPath) { return '/'; } if (0 !== strpos($uriPath, '/')) { return '/'; } if ('/' === $uriPath) { return '/'; } if (0 === $lastSlashPos = strrpos($uriPath, '/')) { return '/'; } return substr($uriPath, 0, $lastSlashPos); } public function withCookieHeader(RequestInterface $request) { $values = []; $uri = $request->getUri(); $scheme = $uri->getScheme(); $host = $uri->getHost(); $path = $uri->getPath() ?: '/'; foreach ($this->cookies as $cookie) { if ($cookie->matchesPath($path) && $cookie->matchesDomain($host) && !$cookie->isExpired() && (!$cookie->getSecure() || $scheme === 'https') ) { $values[] = $cookie->getName() . '=' . $cookie->getValue(); } } return $values ? $request->withHeader('Cookie', implode('; ', $values)) : $request; } private function removeCookieIfEmpty(SetCookie $cookie) { $cookieValue = $cookie->getValue(); if ($cookieValue === null || $cookieValue === '') { $this->clear( $cookie->getDomain(), $cookie->getPath(), $cookie->getName() ); } } } filename = $cookieFile; $this->storeSessionCookies = $storeSessionCookies; if (file_exists($cookieFile)) { $this->load($cookieFile); } } public function __destruct() { $this->save($this->filename); } public function save($filename) { $json = []; foreach ($this as $cookie) { if (CookieJar::shouldPersist($cookie, $this->storeSessionCookies)) { $json[] = $cookie->toArray(); } } $jsonStr = \GuzzleHttp\json_encode($json); if (false === file_put_contents($filename, $jsonStr)) { throw new \RuntimeException("Unable to save file {$filename}"); } } public function load($filename) { $json = file_get_contents($filename); if (false === $json) { throw new \RuntimeException("Unable to load file {$filename}"); } elseif ($json === '') { return; } $data = \GuzzleHttp\json_decode($json, true); if (is_array($data)) { foreach (json_decode($json, true) as $cookie) { $this->setCookie(new SetCookie($cookie)); } } elseif (strlen($data)) { throw new \RuntimeException("Invalid cookie file: {$filename}"); } } } sessionKey = $sessionKey; $this->storeSessionCookies = $storeSessionCookies; $this->load(); } public function __destruct() { $this->save(); } public function save() { $json = []; foreach ($this as $cookie) { if (CookieJar::shouldPersist($cookie, $this->storeSessionCookies)) { $json[] = $cookie->toArray(); } } $_SESSION[$this->sessionKey] = json_encode($json); } protected function load() { if (!isset($_SESSION[$this->sessionKey])) { return; } $data = json_decode($_SESSION[$this->sessionKey], true); if (is_array($data)) { foreach ($data as $cookie) { $this->setCookie(new SetCookie($cookie)); } } elseif (strlen($data)) { throw new \RuntimeException("Invalid cookie data"); } } } null, 'Value' => null, 'Domain' => null, 'Path' => '/', 'Max-Age' => null, 'Expires' => null, 'Secure' => false, 'Discard' => false, 'HttpOnly' => false ]; private $data; public static function fromString($cookie) { $data = self::$defaults; $pieces = array_filter(array_map('trim', explode(';', $cookie))); if (empty($pieces[0]) || !strpos($pieces[0], '=')) { return new self($data); } foreach ($pieces as $part) { $cookieParts = explode('=', $part, 2); $key = trim($cookieParts[0]); $value = isset($cookieParts[1]) ? trim($cookieParts[1], " \n\r\t\0\x0B") : true; if (empty($data['Name'])) { $data['Name'] = $key; $data['Value'] = $value; } else { foreach (array_keys(self::$defaults) as $search) { if (!strcasecmp($search, $key)) { $data[$search] = $value; continue 2; } } $data[$key] = $value; } } return new self($data); } public function __construct(array $data = []) { $this->data = array_replace(self::$defaults, $data); if (!$this->getExpires() && $this->getMaxAge()) { $this->setExpires(time() + $this->getMaxAge()); } elseif ($this->getExpires() && !is_numeric($this->getExpires())) { $this->setExpires($this->getExpires()); } } public function __toString() { $str = $this->data['Name'] . '=' . $this->data['Value'] . '; '; foreach ($this->data as $k => $v) { if ($k !== 'Name' && $k !== 'Value' && $v !== null && $v !== false) { if ($k === 'Expires') { $str .= 'Expires=' . gmdate('D, d M Y H:i:s \G\M\T', $v) . '; '; } else { $str .= ($v === true ? $k : "{$k}={$v}") . '; '; } } } return rtrim($str, '; '); } public function toArray() { return $this->data; } public function getName() { return $this->data['Name']; } public function setName($name) { $this->data['Name'] = $name; } public function getValue() { return $this->data['Value']; } public function setValue($value) { $this->data['Value'] = $value; } public function getDomain() { return $this->data['Domain']; } public function setDomain($domain) { $this->data['Domain'] = $domain; } public function getPath() { return $this->data['Path']; } public function setPath($path) { $this->data['Path'] = $path; } public function getMaxAge() { return $this->data['Max-Age']; } public function setMaxAge($maxAge) { $this->data['Max-Age'] = $maxAge; } public function getExpires() { return $this->data['Expires']; } public function setExpires($timestamp) { $this->data['Expires'] = is_numeric($timestamp) ? (int) $timestamp : strtotime($timestamp); } public function getSecure() { return $this->data['Secure']; } public function setSecure($secure) { $this->data['Secure'] = $secure; } public function getDiscard() { return $this->data['Discard']; } public function setDiscard($discard) { $this->data['Discard'] = $discard; } public function getHttpOnly() { return $this->data['HttpOnly']; } public function setHttpOnly($httpOnly) { $this->data['HttpOnly'] = $httpOnly; } public function matchesPath($requestPath) { $cookiePath = $this->getPath(); if ($cookiePath === '/' || $cookiePath == $requestPath) { return true; } if (0 !== strpos($requestPath, $cookiePath)) { return false; } if (substr($cookiePath, -1, 1) === '/') { return true; } return substr($requestPath, strlen($cookiePath), 1) === '/'; } public function matchesDomain($domain) { $cookieDomain = ltrim($this->getDomain(), '.'); if (!$cookieDomain || !strcasecmp($domain, $cookieDomain)) { return true; } if (filter_var($domain, FILTER_VALIDATE_IP)) { return false; } return (bool) preg_match('/\.' . preg_quote($cookieDomain, '/') . '$/', $domain); } public function isExpired() { return $this->getExpires() !== null && time() > $this->getExpires(); } public function validate() { $name = $this->getName(); if (empty($name) && !is_numeric($name)) { return 'The cookie name must not be empty'; } if (preg_match( '/[\x00-\x20\x22\x28-\x29\x2c\x2f\x3a-\x40\x5c\x7b\x7d\x7f]/', $name )) { return 'Cookie name must not contain invalid characters: ASCII ' . 'Control characters (0-31;127), space, tab and the ' . 'following characters: ()<>@,;:\"/?={}'; } $value = $this->getValue(); if (empty($value) && !is_numeric($value)) { return 'The cookie value must not be empty'; } $domain = $this->getDomain(); if (empty($domain) && !is_numeric($domain)) { return 'The cookie domain must not be empty'; } return true; } } getStatusCode() : 0; parent::__construct($message, $code, $previous); $this->request = $request; $this->response = $response; $this->handlerContext = $handlerContext; } public static function wrapException(RequestInterface $request, \Exception $e) { return $e instanceof RequestException ? $e : new RequestException($e->getMessage(), $request, null, $e); } public static function create( RequestInterface $request, ResponseInterface $response = null, \Exception $previous = null, array $ctx = [] ) { if (!$response) { return new self( 'Error completing request', $request, null, $previous, $ctx ); } $level = (int) floor($response->getStatusCode() / 100); if ($level === 4) { $label = 'Client error'; $className = ClientException::class; } elseif ($level === 5) { $label = 'Server error'; $className = ServerException::class; } else { $label = 'Unsuccessful request'; $className = __CLASS__; } $uri = $request->getUri(); $uri = static::obfuscateUri($uri); $message = sprintf( '%s: `%s %s` resulted in a `%s %s` response', $label, $request->getMethod(), $uri, $response->getStatusCode(), $response->getReasonPhrase() ); $summary = static::getResponseBodySummary($response); if ($summary !== null) { $message .= ":\n{$summary}\n"; } return new $className($message, $request, $response, $previous, $ctx); } public static function getResponseBodySummary(ResponseInterface $response) { $body = $response->getBody(); if (!$body->isSeekable()) { return null; } $size = $body->getSize(); if ($size === 0) { return null; } $summary = $body->read(120); $body->rewind(); if ($size > 120) { $summary .= ' (truncated...)'; } if (preg_match('/[^\pL\pM\pN\pP\pS\pZ\n\r\t]/', $summary)) { return null; } return $summary; } private static function obfuscateUri($uri) { $userInfo = $uri->getUserInfo(); if (false !== ($pos = strpos($userInfo, ':'))) { return $uri->withUserInfo(substr($userInfo, 0, $pos), '***'); } return $uri; } public function getRequest() { return $this->request; } public function getResponse() { return $this->response; } public function hasResponse() { return $this->response !== null; } public function getHandlerContext() { return $this->handlerContext; } } stream = $stream; $msg = $msg ?: 'Could not seek the stream to position ' . $pos; parent::__construct($msg); } public function getStream() { return $this->stream; } } expand($template, $variables); } function describe_type($input) { switch (gettype($input)) { case 'object': return 'object(' . get_class($input) . ')'; case 'array': return 'array(' . count($input) . ')'; default: ob_start(); var_dump($input); return str_replace('double(', 'float(', rtrim(ob_get_clean())); } } function headers_from_lines($lines) { $headers = []; foreach ($lines as $line) { $parts = explode(':', $line, 2); $headers[trim($parts[0])][] = isset($parts[1]) ? trim($parts[1]) : null; } return $headers; } function debug_resource($value = null) { if (is_resource($value)) { return $value; } elseif (defined('STDOUT')) { return STDOUT; } return fopen('php://output', 'w'); } function choose_handler() { $handler = null; if (function_exists('curl_multi_exec') && function_exists('curl_exec')) { $handler = Proxy::wrapSync(new CurlMultiHandler(), new CurlHandler()); } elseif (function_exists('curl_exec')) { $handler = new CurlHandler(); } elseif (function_exists('curl_multi_exec')) { $handler = new CurlMultiHandler(); } if (ini_get('allow_url_fopen')) { $handler = $handler ? Proxy::wrapStreaming($handler, new StreamHandler()) : new StreamHandler(); } elseif (!$handler) { throw new \RuntimeException('GuzzleHttp requires cURL, the ' . 'allow_url_fopen ini setting, or a custom HTTP handler.'); } return $handler; } function default_user_agent() { static $defaultAgent = ''; if (!$defaultAgent) { $defaultAgent = 'GuzzleHttp/' . Client::VERSION; if (extension_loaded('curl') && function_exists('curl_version')) { $defaultAgent .= ' curl/' . \curl_version()['version']; } $defaultAgent .= ' PHP/' . PHP_VERSION; } return $defaultAgent; } function default_ca_bundle() { static $cached = null; static $cafiles = [ '/etc/pki/tls/certs/ca-bundle.crt', '/etc/ssl/certs/ca-certificates.crt', '/usr/local/share/certs/ca-root-nss.crt', '/var/lib/ca-certificates/ca-bundle.pem', '/usr/local/etc/openssl/cert.pem', '/etc/ca-certificates.crt', 'C:\\windows\\system32\\curl-ca-bundle.crt', 'C:\\windows\\curl-ca-bundle.crt', ]; if ($cached) { return $cached; } if ($ca = ini_get('openssl.cafile')) { return $cached = $ca; } if ($ca = ini_get('curl.cainfo')) { return $cached = $ca; } foreach ($cafiles as $filename) { if (file_exists($filename)) { return $cached = $filename; } } throw new \RuntimeException(<<< EOT No system CA bundle could be found in any of the the common system locations. PHP versions earlier than 5.6 are not properly configured to use the system's CA bundle by default. In order to verify peer certificates, you will need to supply the path on disk to a certificate bundle to the 'verify' request option: http://docs.guzzlephp.org/en/latest/clients.html#verify. If you do not need a specific certificate bundle, then Mozilla provides a commonly used CA bundle which can be downloaded here (provided by the maintainer of cURL): https://raw.githubusercontent.com/bagder/ca-bundle/master/ca-bundle.crt. Once you have a CA bundle available on disk, you can set the 'openssl.cafile' PHP ini setting to point to the path to the file, allowing you to omit the 'verify' request option. See http://curl.haxx.se/docs/sslcerts.html for more information. EOT ); } function normalize_header_keys(array $headers) { $result = []; foreach (array_keys($headers) as $key) { $result[strtolower($key)] = $key; } return $result; } function is_host_in_noproxy($host, array $noProxyArray) { if (strlen($host) === 0) { throw new \InvalidArgumentException('Empty host provided'); } if (strpos($host, ':')) { $host = explode($host, ':', 2)[0]; } foreach ($noProxyArray as $area) { if ($area === '*') { return true; } elseif (empty($area)) { continue; } elseif ($area === $host) { return true; } else { $area = '.' . ltrim($area, '.'); if (substr($host, -(strlen($area))) === $area) { return true; } } } return false; } function json_decode($json, $assoc = false, $depth = 512, $options = 0) { $data = \json_decode($json, $assoc, $depth, $options); if (JSON_ERROR_NONE !== json_last_error()) { throw new \InvalidArgumentException( 'json_decode error: ' . json_last_error_msg() ); } return $data; } function json_encode($value, $options = 0, $depth = 512) { $json = \json_encode($value, $options, $depth); if (JSON_ERROR_NONE !== json_last_error()) { throw new \InvalidArgumentException( 'json_encode error: ' . json_last_error_msg() ); } return $json; } maxHandles = $maxHandles; } public function create(RequestInterface $request, array $options) { if (isset($options['curl']['body_as_string'])) { $options['_body_as_string'] = $options['curl']['body_as_string']; unset($options['curl']['body_as_string']); } $easy = new EasyHandle; $easy->request = $request; $easy->options = $options; $conf = $this->getDefaultConf($easy); $this->applyMethod($easy, $conf); $this->applyHandlerOptions($easy, $conf); $this->applyHeaders($easy, $conf); unset($conf['_headers']); if (isset($options['curl'])) { $conf = array_replace($conf, $options['curl']); } $conf[CURLOPT_HEADERFUNCTION] = $this->createHeaderFn($easy); $easy->handle = $this->handles ? array_pop($this->handles) : curl_init(); curl_setopt_array($easy->handle, $conf); return $easy; } public function release(EasyHandle $easy) { $resource = $easy->handle; unset($easy->handle); if (count($this->handles) >= $this->maxHandles) { curl_close($resource); } else { curl_setopt($resource, CURLOPT_HEADERFUNCTION, null); curl_setopt($resource, CURLOPT_READFUNCTION, null); curl_setopt($resource, CURLOPT_WRITEFUNCTION, null); curl_setopt($resource, CURLOPT_PROGRESSFUNCTION, null); curl_reset($resource); $this->handles[] = $resource; } } public static function finish( callable $handler, EasyHandle $easy, CurlFactoryInterface $factory ) { if (isset($easy->options['on_stats'])) { self::invokeStats($easy); } if (!$easy->response || $easy->errno) { return self::finishError($handler, $easy, $factory); } $factory->release($easy); $body = $easy->response->getBody(); if ($body->isSeekable()) { $body->rewind(); } return new FulfilledPromise($easy->response); } private static function invokeStats(EasyHandle $easy) { $curlStats = curl_getinfo($easy->handle); $stats = new TransferStats( $easy->request, $easy->response, $curlStats['total_time'], $easy->errno, $curlStats ); call_user_func($easy->options['on_stats'], $stats); } private static function finishError( callable $handler, EasyHandle $easy, CurlFactoryInterface $factory ) { $ctx = [ 'errno' => $easy->errno, 'error' => curl_error($easy->handle), ] + curl_getinfo($easy->handle); $factory->release($easy); if (empty($easy->options['_err_message']) && (!$easy->errno || $easy->errno == 65) ) { return self::retryFailedRewind($handler, $easy, $ctx); } return self::createRejection($easy, $ctx); } private static function createRejection(EasyHandle $easy, array $ctx) { static $connectionErrors = [ CURLE_OPERATION_TIMEOUTED => true, CURLE_COULDNT_RESOLVE_HOST => true, CURLE_COULDNT_CONNECT => true, CURLE_SSL_CONNECT_ERROR => true, CURLE_GOT_NOTHING => true, ]; if ($easy->onHeadersException) { return \GuzzleHttp\Promise\rejection_for( new RequestException( 'An error was encountered during the on_headers event', $easy->request, $easy->response, $easy->onHeadersException, $ctx ) ); } $message = sprintf( 'cURL error %s: %s (%s)', $ctx['errno'], $ctx['error'], 'see http://curl.haxx.se/libcurl/c/libcurl-errors.html' ); $error = isset($connectionErrors[$easy->errno]) ? new ConnectException($message, $easy->request, null, $ctx) : new RequestException($message, $easy->request, $easy->response, null, $ctx); return \GuzzleHttp\Promise\rejection_for($error); } private function getDefaultConf(EasyHandle $easy) { $conf = [ '_headers' => $easy->request->getHeaders(), CURLOPT_CUSTOMREQUEST => $easy->request->getMethod(), CURLOPT_URL => (string) $easy->request->getUri()->withFragment(''), CURLOPT_RETURNTRANSFER => false, CURLOPT_HEADER => false, CURLOPT_CONNECTTIMEOUT => 150, ]; if (defined('CURLOPT_PROTOCOLS')) { $conf[CURLOPT_PROTOCOLS] = CURLPROTO_HTTP | CURLPROTO_HTTPS; } $version = $easy->request->getProtocolVersion(); if ($version == 1.1) { $conf[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_1_1; } elseif ($version == 2.0) { $conf[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_2_0; } else { $conf[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_1_0; } return $conf; } private function applyMethod(EasyHandle $easy, array &$conf) { $body = $easy->request->getBody(); $size = $body->getSize(); if ($size === null || $size > 0) { $this->applyBody($easy->request, $easy->options, $conf); return; } $method = $easy->request->getMethod(); if ($method === 'PUT' || $method === 'POST') { if (!$easy->request->hasHeader('Content-Length')) { $conf[CURLOPT_HTTPHEADER][] = 'Content-Length: 0'; } } elseif ($method === 'HEAD') { $conf[CURLOPT_NOBODY] = true; unset( $conf[CURLOPT_WRITEFUNCTION], $conf[CURLOPT_READFUNCTION], $conf[CURLOPT_FILE], $conf[CURLOPT_INFILE] ); } } private function applyBody(RequestInterface $request, array $options, array &$conf) { $size = $request->hasHeader('Content-Length') ? (int) $request->getHeaderLine('Content-Length') : null; if (($size !== null && $size < 1000000) || !empty($options['_body_as_string']) ) { $conf[CURLOPT_POSTFIELDS] = (string) $request->getBody(); $this->removeHeader('Content-Length', $conf); $this->removeHeader('Transfer-Encoding', $conf); } else { $conf[CURLOPT_UPLOAD] = true; if ($size !== null) { $conf[CURLOPT_INFILESIZE] = $size; $this->removeHeader('Content-Length', $conf); } $body = $request->getBody(); if ($body->isSeekable()) { $body->rewind(); } $conf[CURLOPT_READFUNCTION] = function ($ch, $fd, $length) use ($body) { return $body->read($length); }; } if (!$request->hasHeader('Expect')) { $conf[CURLOPT_HTTPHEADER][] = 'Expect:'; } if (!$request->hasHeader('Content-Type')) { $conf[CURLOPT_HTTPHEADER][] = 'Content-Type:'; } } private function applyHeaders(EasyHandle $easy, array &$conf) { foreach ($conf['_headers'] as $name => $values) { foreach ($values as $value) { $value = (string) $value; if ($value === '') { $conf[CURLOPT_HTTPHEADER][] = "$name;"; } else { $conf[CURLOPT_HTTPHEADER][] = "$name: $value"; } } } if (!$easy->request->hasHeader('Accept')) { $conf[CURLOPT_HTTPHEADER][] = 'Accept:'; } } private function removeHeader($name, array &$options) { foreach (array_keys($options['_headers']) as $key) { if (!strcasecmp($key, $name)) { unset($options['_headers'][$key]); return; } } } private function applyHandlerOptions(EasyHandle $easy, array &$conf) { $options = $easy->options; if (isset($options['verify'])) { if ($options['verify'] === false) { unset($conf[CURLOPT_CAINFO]); $conf[CURLOPT_SSL_VERIFYHOST] = 0; $conf[CURLOPT_SSL_VERIFYPEER] = false; } else { $conf[CURLOPT_SSL_VERIFYHOST] = 2; $conf[CURLOPT_SSL_VERIFYPEER] = true; if (is_string($options['verify'])) { if (!file_exists($options['verify'])) { throw new \InvalidArgumentException( "SSL CA bundle not found: {$options['verify']}" ); } if (is_dir($options['verify']) || (is_link($options['verify']) && is_dir(readlink($options['verify'])))) { $conf[CURLOPT_CAPATH] = $options['verify']; } else { $conf[CURLOPT_CAINFO] = $options['verify']; } } } } if (!empty($options['decode_content'])) { $accept = $easy->request->getHeaderLine('Accept-Encoding'); if ($accept) { $conf[CURLOPT_ENCODING] = $accept; } else { $conf[CURLOPT_ENCODING] = ''; $conf[CURLOPT_HTTPHEADER][] = 'Accept-Encoding:'; } } if (isset($options['sink'])) { $sink = $options['sink']; if (!is_string($sink)) { $sink = \GuzzleHttp\Psr7\stream_for($sink); } elseif (!is_dir(dirname($sink))) { throw new \RuntimeException(sprintf( 'Directory %s does not exist for sink value of %s', dirname($sink), $sink )); } else { $sink = new LazyOpenStream($sink, 'w+'); } $easy->sink = $sink; $conf[CURLOPT_WRITEFUNCTION] = function ($ch, $write) use ($sink) { return $sink->write($write); }; } else { $conf[CURLOPT_FILE] = fopen('php://temp', 'w+'); $easy->sink = Psr7\stream_for($conf[CURLOPT_FILE]); } $timeoutRequiresNoSignal = false; if (isset($options['timeout'])) { $timeoutRequiresNoSignal |= $options['timeout'] < 1; $conf[CURLOPT_TIMEOUT_MS] = $options['timeout'] * 1000; } if (isset($options['force_ip_resolve'])) { if ('v4' === $options['force_ip_resolve']) { $conf[CURLOPT_IPRESOLVE] = CURL_IPRESOLVE_V4; } elseif ('v6' === $options['force_ip_resolve']) { $conf[CURLOPT_IPRESOLVE] = CURL_IPRESOLVE_V6; } } if (isset($options['connect_timeout'])) { $timeoutRequiresNoSignal |= $options['connect_timeout'] < 1; $conf[CURLOPT_CONNECTTIMEOUT_MS] = $options['connect_timeout'] * 1000; } if ($timeoutRequiresNoSignal && strtoupper(substr(PHP_OS, 0, 3)) !== 'WIN') { $conf[CURLOPT_NOSIGNAL] = true; } if (isset($options['proxy'])) { if (!is_array($options['proxy'])) { $conf[CURLOPT_PROXY] = $options['proxy']; } else { $scheme = $easy->request->getUri()->getScheme(); if (isset($options['proxy'][$scheme])) { $host = $easy->request->getUri()->getHost(); if (!isset($options['proxy']['no']) || !\GuzzleHttp\is_host_in_noproxy($host, $options['proxy']['no']) ) { $conf[CURLOPT_PROXY] = $options['proxy'][$scheme]; } } } } if (isset($options['cert'])) { $cert = $options['cert']; if (is_array($cert)) { $conf[CURLOPT_SSLCERTPASSWD] = $cert[1]; $cert = $cert[0]; } if (!file_exists($cert)) { throw new \InvalidArgumentException( "SSL certificate not found: {$cert}" ); } $conf[CURLOPT_SSLCERT] = $cert; } if (isset($options['ssl_key'])) { $sslKey = $options['ssl_key']; if (is_array($sslKey)) { $conf[CURLOPT_SSLKEYPASSWD] = $sslKey[1]; $sslKey = $sslKey[0]; } if (!file_exists($sslKey)) { throw new \InvalidArgumentException( "SSL private key not found: {$sslKey}" ); } $conf[CURLOPT_SSLKEY] = $sslKey; } if (isset($options['progress'])) { $progress = $options['progress']; if (!is_callable($progress)) { throw new \InvalidArgumentException( 'progress client option must be callable' ); } $conf[CURLOPT_NOPROGRESS] = false; $conf[CURLOPT_PROGRESSFUNCTION] = function () use ($progress) { $args = func_get_args(); if (is_resource($args[0])) { array_shift($args); } call_user_func_array($progress, $args); }; } if (!empty($options['debug'])) { $conf[CURLOPT_STDERR] = \GuzzleHttp\debug_resource($options['debug']); $conf[CURLOPT_VERBOSE] = true; } } private static function retryFailedRewind( callable $handler, EasyHandle $easy, array $ctx ) { try { $body = $easy->request->getBody(); if ($body->tell() > 0) { $body->rewind(); } } catch (\RuntimeException $e) { $ctx['error'] = 'The connection unexpectedly failed without ' . 'providing an error. The request would have been retried, ' . 'but attempting to rewind the request body failed. ' . 'Exception: ' . $e; return self::createRejection($easy, $ctx); } if (!isset($easy->options['_curl_retries'])) { $easy->options['_curl_retries'] = 1; } elseif ($easy->options['_curl_retries'] == 2) { $ctx['error'] = 'The cURL request was retried 3 times ' . 'and did not succeed. The most likely reason for the failure ' . 'is that cURL was unable to rewind the body of the request ' . 'and subsequent retries resulted in the same error. Turn on ' . 'the debug option to see what went wrong. See ' . 'https://bugs.php.net/bug.php?id=47204 for more information.'; return self::createRejection($easy, $ctx); } else { $easy->options['_curl_retries']++; } return $handler($easy->request, $easy->options); } private function createHeaderFn(EasyHandle $easy) { if (isset($easy->options['on_headers'])) { $onHeaders = $easy->options['on_headers']; if (!is_callable($onHeaders)) { throw new \InvalidArgumentException('on_headers must be callable'); } } else { $onHeaders = null; } return function ($ch, $h) use ( $onHeaders, $easy, &$startingResponse ) { $value = trim($h); if ($value === '') { $startingResponse = true; $easy->createResponse(); if ($onHeaders !== null) { try { $onHeaders($easy->response); } catch (\Exception $e) { $easy->onHeadersException = $e; return -1; } } } elseif ($startingResponse) { $startingResponse = false; $easy->headers = [$value]; } else { $easy->headers[] = $value; } return strlen($h); }; } } factory = isset($options['handle_factory']) ? $options['handle_factory'] : new CurlFactory(3); } public function __invoke(RequestInterface $request, array $options) { if (isset($options['delay'])) { usleep($options['delay'] * 1000); } $easy = $this->factory->create($request, $options); curl_exec($easy->handle); $easy->errno = curl_errno($easy->handle); return CurlFactory::finish($this, $easy, $this->factory); } } factory = isset($options['handle_factory']) ? $options['handle_factory'] : new CurlFactory(50); $this->selectTimeout = isset($options['select_timeout']) ? $options['select_timeout'] : 1; } public function __get($name) { if ($name === '_mh') { return $this->_mh = curl_multi_init(); } throw new \BadMethodCallException(); } public function __destruct() { if (isset($this->_mh)) { curl_multi_close($this->_mh); unset($this->_mh); } } public function __invoke(RequestInterface $request, array $options) { $easy = $this->factory->create($request, $options); $id = (int) $easy->handle; $promise = new Promise( [$this, 'execute'], function () use ($id) { return $this->cancel($id); } ); $this->addRequest(['easy' => $easy, 'deferred' => $promise]); return $promise; } public function tick() { if ($this->delays) { $currentTime = microtime(true); foreach ($this->delays as $id => $delay) { if ($currentTime >= $delay) { unset($this->delays[$id]); curl_multi_add_handle( $this->_mh, $this->handles[$id]['easy']->handle ); } } } P\queue()->run(); if ($this->active && curl_multi_select($this->_mh, $this->selectTimeout) === -1 ) { usleep(250); } while (curl_multi_exec($this->_mh, $this->active) === CURLM_CALL_MULTI_PERFORM); $this->processMessages(); } public function execute() { $queue = P\queue(); while ($this->handles || !$queue->isEmpty()) { if (!$this->active && $this->delays) { usleep($this->timeToNext()); } $this->tick(); } } private function addRequest(array $entry) { $easy = $entry['easy']; $id = (int) $easy->handle; $this->handles[$id] = $entry; if (empty($easy->options['delay'])) { curl_multi_add_handle($this->_mh, $easy->handle); } else { $this->delays[$id] = microtime(true) + ($easy->options['delay'] / 1000); } } private function cancel($id) { if (!isset($this->handles[$id])) { return false; } $handle = $this->handles[$id]['easy']->handle; unset($this->delays[$id], $this->handles[$id]); curl_multi_remove_handle($this->_mh, $handle); curl_close($handle); return true; } private function processMessages() { while ($done = curl_multi_info_read($this->_mh)) { $id = (int) $done['handle']; curl_multi_remove_handle($this->_mh, $done['handle']); if (!isset($this->handles[$id])) { continue; } $entry = $this->handles[$id]; unset($this->handles[$id], $this->delays[$id]); $entry['easy']->errno = $done['result']; $entry['deferred']->resolve( CurlFactory::finish( $this, $entry['easy'], $this->factory ) ); } } private function timeToNext() { $currentTime = microtime(true); $nextTime = PHP_INT_MAX; foreach ($this->delays as $time) { if ($time < $nextTime) { $nextTime = $time; } } return max(0, $nextTime - $currentTime) * 1000000; } } headers)) { throw new \RuntimeException('No headers have been received'); } $startLine = explode(' ', array_shift($this->headers), 3); $headers = \GuzzleHttp\headers_from_lines($this->headers); $normalizedKeys = \GuzzleHttp\normalize_header_keys($headers); if (!empty($this->options['decode_content']) && isset($normalizedKeys['content-encoding']) ) { $headers['x-encoded-content-encoding'] = $headers[$normalizedKeys['content-encoding']]; unset($headers[$normalizedKeys['content-encoding']]); if (isset($normalizedKeys['content-length'])) { $headers['x-encoded-content-length'] = $headers[$normalizedKeys['content-length']]; $bodyLength = (int) $this->sink->getSize(); if ($bodyLength) { $headers[$normalizedKeys['content-length']] = $bodyLength; } else { unset($headers[$normalizedKeys['content-length']]); } } } $this->response = new Response( $startLine[1], $headers, $this->sink, substr($startLine[0], 5), isset($startLine[2]) ? (string) $startLine[2] : null ); } public function __get($name) { $msg = $name === 'handle' ? 'The EasyHandle has been released' : 'Invalid property: ' . $name; throw new \BadMethodCallException($msg); } } onFulfilled = $onFulfilled; $this->onRejected = $onRejected; if ($queue) { call_user_func_array([$this, 'append'], $queue); } } public function __invoke(RequestInterface $request, array $options) { if (!$this->queue) { throw new \OutOfBoundsException('Mock queue is empty'); } if (isset($options['delay'])) { usleep($options['delay'] * 1000); } $this->lastRequest = $request; $this->lastOptions = $options; $response = array_shift($this->queue); if (isset($options['on_headers'])) { if (!is_callable($options['on_headers'])) { throw new \InvalidArgumentException('on_headers must be callable'); } try { $options['on_headers']($response); } catch (\Exception $e) { $msg = 'An error was encountered during the on_headers event'; $response = new RequestException($msg, $request, $response, $e); } } if (is_callable($response)) { $response = call_user_func($response, $request, $options); } $response = $response instanceof \Exception ? \GuzzleHttp\Promise\rejection_for($response) : \GuzzleHttp\Promise\promise_for($response); return $response->then( function ($value) use ($request, $options) { $this->invokeStats($request, $options, $value); if ($this->onFulfilled) { call_user_func($this->onFulfilled, $value); } if (isset($options['sink'])) { $contents = (string) $value->getBody(); $sink = $options['sink']; if (is_resource($sink)) { fwrite($sink, $contents); } elseif (is_string($sink)) { file_put_contents($sink, $contents); } elseif ($sink instanceof \Psr\Http\Message\StreamInterface) { $sink->write($contents); } } return $value; }, function ($reason) use ($request, $options) { $this->invokeStats($request, $options, null, $reason); if ($this->onRejected) { call_user_func($this->onRejected, $reason); } return \GuzzleHttp\Promise\rejection_for($reason); } ); } public function append() { foreach (func_get_args() as $value) { if ($value instanceof ResponseInterface || $value instanceof \Exception || $value instanceof PromiseInterface || is_callable($value) ) { $this->queue[] = $value; } else { throw new \InvalidArgumentException('Expected a response or ' . 'exception. Found ' . \GuzzleHttp\describe_type($value)); } } } public function getLastRequest() { return $this->lastRequest; } public function getLastOptions() { return $this->lastOptions; } public function count() { return count($this->queue); } private function invokeStats( RequestInterface $request, array $options, ResponseInterface $response = null, $reason = null ) { if (isset($options['on_stats'])) { $stats = new TransferStats($request, $response, 0, $reason); call_user_func($options['on_stats'], $stats); } } } withoutHeader('Expect'); if (0 === $request->getBody()->getSize()) { $request = $request->withHeader('Content-Length', 0); } return $this->createResponse( $request, $options, $this->createStream($request, $options), $startTime ); } catch (\InvalidArgumentException $e) { throw $e; } catch (\Exception $e) { $message = $e->getMessage(); if (strpos($message, 'getaddrinfo') || strpos($message, 'Connection refused') || strpos($message, "couldn't connect to host") || strpos($message, "connection attempt failed") ) { $e = new ConnectException($e->getMessage(), $request, $e); } $e = RequestException::wrapException($request, $e); $this->invokeStats($options, $request, $startTime, null, $e); return \GuzzleHttp\Promise\rejection_for($e); } } private function invokeStats( array $options, RequestInterface $request, $startTime, ResponseInterface $response = null, $error = null ) { if (isset($options['on_stats'])) { $stats = new TransferStats( $request, $response, microtime(true) - $startTime, $error, [] ); call_user_func($options['on_stats'], $stats); } } private function createResponse( RequestInterface $request, array $options, $stream, $startTime ) { $hdrs = $this->lastHeaders; $this->lastHeaders = []; $parts = explode(' ', array_shift($hdrs), 3); $ver = explode('/', $parts[0])[1]; $status = $parts[1]; $reason = isset($parts[2]) ? $parts[2] : null; $headers = \GuzzleHttp\headers_from_lines($hdrs); list($stream, $headers) = $this->checkDecode($options, $headers, $stream); $stream = Psr7\stream_for($stream); $sink = $stream; if (strcasecmp('HEAD', $request->getMethod())) { $sink = $this->createSink($stream, $options); } $response = new Psr7\Response($status, $headers, $sink, $ver, $reason); if (isset($options['on_headers'])) { try { $options['on_headers']($response); } catch (\Exception $e) { $msg = 'An error was encountered during the on_headers event'; $ex = new RequestException($msg, $request, $response, $e); return \GuzzleHttp\Promise\rejection_for($ex); } } if ($sink !== $stream) { $this->drain( $stream, $sink, $response->getHeaderLine('Content-Length') ); } $this->invokeStats($options, $request, $startTime, $response, null); return new FulfilledPromise($response); } private function createSink(StreamInterface $stream, array $options) { if (!empty($options['stream'])) { return $stream; } $sink = isset($options['sink']) ? $options['sink'] : fopen('php://temp', 'r+'); return is_string($sink) ? new Psr7\LazyOpenStream($sink, 'w+') : Psr7\stream_for($sink); } private function checkDecode(array $options, array $headers, $stream) { if (!empty($options['decode_content'])) { $normalizedKeys = \GuzzleHttp\normalize_header_keys($headers); if (isset($normalizedKeys['content-encoding'])) { $encoding = $headers[$normalizedKeys['content-encoding']]; if ($encoding[0] === 'gzip' || $encoding[0] === 'deflate') { $stream = new Psr7\InflateStream( Psr7\stream_for($stream) ); $headers['x-encoded-content-encoding'] = $headers[$normalizedKeys['content-encoding']]; unset($headers[$normalizedKeys['content-encoding']]); if (isset($normalizedKeys['content-length'])) { $headers['x-encoded-content-length'] = $headers[$normalizedKeys['content-length']]; $length = (int) $stream->getSize(); if ($length === 0) { unset($headers[$normalizedKeys['content-length']]); } else { $headers[$normalizedKeys['content-length']] = [$length]; } } } } } return [$stream, $headers]; } private function drain( StreamInterface $source, StreamInterface $sink, $contentLength ) { Psr7\copy_to_stream( $source, $sink, (strlen($contentLength) > 0 && (int) $contentLength > 0) ? (int) $contentLength : -1 ); $sink->seek(0); $source->close(); return $sink; } private function createResource(callable $callback) { $errors = null; set_error_handler(function ($_, $msg, $file, $line) use (&$errors) { $errors[] = [ 'message' => $msg, 'file' => $file, 'line' => $line ]; return true; }); $resource = $callback(); restore_error_handler(); if (!$resource) { $message = 'Error creating resource: '; foreach ($errors as $err) { foreach ($err as $key => $value) { $message .= "[$key] $value" . PHP_EOL; } } throw new \RuntimeException(trim($message)); } return $resource; } private function createStream(RequestInterface $request, array $options) { static $methods; if (!$methods) { $methods = array_flip(get_class_methods(__CLASS__)); } if ($request->getProtocolVersion() == '1.1' && !$request->hasHeader('Connection') ) { $request = $request->withHeader('Connection', 'close'); } if (!isset($options['verify'])) { $options['verify'] = true; } $params = []; $context = $this->getDefaultContext($request); if (isset($options['on_headers']) && !is_callable($options['on_headers'])) { throw new \InvalidArgumentException('on_headers must be callable'); } if (!empty($options)) { foreach ($options as $key => $value) { $method = "add_{$key}"; if (isset($methods[$method])) { $this->{$method}($request, $context, $value, $params); } } } if (isset($options['stream_context'])) { if (!is_array($options['stream_context'])) { throw new \InvalidArgumentException('stream_context must be an array'); } $context = array_replace_recursive( $context, $options['stream_context'] ); } if (isset($options['auth']) && is_array($options['auth']) && isset($options['auth'][2]) && 'ntlm' == $options['auth'][2] ) { throw new \InvalidArgumentException('Microsoft NTLM authentication only supported with curl handler'); } $uri = $this->resolveHost($request, $options); $context = $this->createResource( function () use ($context, $params) { return stream_context_create($context, $params); } ); return $this->createResource( function () use ($uri, &$http_response_header, $context, $options) { $resource = fopen((string) $uri, 'r', null, $context); $this->lastHeaders = $http_response_header; if (isset($options['read_timeout'])) { $readTimeout = $options['read_timeout']; $sec = (int) $readTimeout; $usec = ($readTimeout - $sec) * 100000; stream_set_timeout($resource, $sec, $usec); } return $resource; } ); } private function resolveHost(RequestInterface $request, array $options) { $uri = $request->getUri(); if (isset($options['force_ip_resolve']) && !filter_var($uri->getHost(), FILTER_VALIDATE_IP)) { if ('v4' === $options['force_ip_resolve']) { $records = dns_get_record($uri->getHost(), DNS_A); if (!isset($records[0]['ip'])) { throw new ConnectException(sprintf("Could not resolve IPv4 address for host '%s'", $uri->getHost()), $request); } $uri = $uri->withHost($records[0]['ip']); } elseif ('v6' === $options['force_ip_resolve']) { $records = dns_get_record($uri->getHost(), DNS_AAAA); if (!isset($records[0]['ipv6'])) { throw new ConnectException(sprintf("Could not resolve IPv6 address for host '%s'", $uri->getHost()), $request); } $uri = $uri->withHost('[' . $records[0]['ipv6'] . ']'); } } return $uri; } private function getDefaultContext(RequestInterface $request) { $headers = ''; foreach ($request->getHeaders() as $name => $value) { foreach ($value as $val) { $headers .= "$name: $val\r\n"; } } $context = [ 'http' => [ 'method' => $request->getMethod(), 'header' => $headers, 'protocol_version' => $request->getProtocolVersion(), 'ignore_errors' => true, 'follow_location' => 0, ], ]; $body = (string) $request->getBody(); if (!empty($body)) { $context['http']['content'] = $body; if (!$request->hasHeader('Content-Type')) { $context['http']['header'] .= "Content-Type:\r\n"; } } $context['http']['header'] = rtrim($context['http']['header']); return $context; } private function add_proxy(RequestInterface $request, &$options, $value, &$params) { if (!is_array($value)) { $options['http']['proxy'] = $value; } else { $scheme = $request->getUri()->getScheme(); if (isset($value[$scheme])) { if (!isset($value['no']) || !\GuzzleHttp\is_host_in_noproxy( $request->getUri()->getHost(), $value['no'] ) ) { $options['http']['proxy'] = $value[$scheme]; } } } } private function add_timeout(RequestInterface $request, &$options, $value, &$params) { if ($value > 0) { $options['http']['timeout'] = $value; } } private function add_verify(RequestInterface $request, &$options, $value, &$params) { if ($value === true) { if (PHP_VERSION_ID < 50600) { $options['ssl']['cafile'] = \GuzzleHttp\default_ca_bundle(); } } elseif (is_string($value)) { $options['ssl']['cafile'] = $value; if (!file_exists($value)) { throw new \RuntimeException("SSL CA bundle not found: $value"); } } elseif ($value === false) { $options['ssl']['verify_peer'] = false; $options['ssl']['verify_peer_name'] = false; return; } else { throw new \InvalidArgumentException('Invalid verify request option'); } $options['ssl']['verify_peer'] = true; $options['ssl']['verify_peer_name'] = true; $options['ssl']['allow_self_signed'] = false; } private function add_cert(RequestInterface $request, &$options, $value, &$params) { if (is_array($value)) { $options['ssl']['passphrase'] = $value[1]; $value = $value[0]; } if (!file_exists($value)) { throw new \RuntimeException("SSL certificate not found: {$value}"); } $options['ssl']['local_cert'] = $value; } private function add_progress(RequestInterface $request, &$options, $value, &$params) { $this->addNotification( $params, function ($code, $a, $b, $c, $transferred, $total) use ($value) { if ($code == STREAM_NOTIFY_PROGRESS) { $value($total, $transferred, null, null); } } ); } private function add_debug(RequestInterface $request, &$options, $value, &$params) { if ($value === false) { return; } static $map = [ STREAM_NOTIFY_CONNECT => 'CONNECT', STREAM_NOTIFY_AUTH_REQUIRED => 'AUTH_REQUIRED', STREAM_NOTIFY_AUTH_RESULT => 'AUTH_RESULT', STREAM_NOTIFY_MIME_TYPE_IS => 'MIME_TYPE_IS', STREAM_NOTIFY_FILE_SIZE_IS => 'FILE_SIZE_IS', STREAM_NOTIFY_REDIRECTED => 'REDIRECTED', STREAM_NOTIFY_PROGRESS => 'PROGRESS', STREAM_NOTIFY_FAILURE => 'FAILURE', STREAM_NOTIFY_COMPLETED => 'COMPLETED', STREAM_NOTIFY_RESOLVE => 'RESOLVE', ]; static $args = ['severity', 'message', 'message_code', 'bytes_transferred', 'bytes_max']; $value = \GuzzleHttp\debug_resource($value); $ident = $request->getMethod() . ' ' . $request->getUri()->withFragment(''); $this->addNotification( $params, function () use ($ident, $value, $map, $args) { $passed = func_get_args(); $code = array_shift($passed); fprintf($value, '<%s> [%s] ', $ident, $map[$code]); foreach (array_filter($passed) as $i => $v) { fwrite($value, $args[$i] . ': "' . $v . '" '); } fwrite($value, "\n"); } ); } private function addNotification(array &$params, callable $notify) { if (!isset($params['notification'])) { $params['notification'] = $notify; } else { $params['notification'] = $this->callArray([ $params['notification'], $notify ]); } } private function callArray(array $functions) { return function () use ($functions) { $args = func_get_args(); foreach ($functions as $fn) { call_user_func_array($fn, $args); } }; } } push(Middleware::httpErrors(), 'http_errors'); $stack->push(Middleware::redirect(), 'allow_redirects'); $stack->push(Middleware::cookies(), 'cookies'); $stack->push(Middleware::prepareBody(), 'prepare_body'); return $stack; } public function __construct(callable $handler = null) { $this->handler = $handler; } public function __invoke(RequestInterface $request, array $options) { $handler = $this->resolve(); return $handler($request, $options); } public function __toString() { $depth = 0; $stack = []; if ($this->handler) { $stack[] = "0) Handler: " . $this->debugCallable($this->handler); } $result = ''; foreach (array_reverse($this->stack) as $tuple) { $depth++; $str = "{$depth}) Name: '{$tuple[1]}', "; $str .= "Function: " . $this->debugCallable($tuple[0]); $result = "> {$str}\n{$result}"; $stack[] = $str; } foreach (array_keys($stack) as $k) { $result .= "< {$stack[$k]}\n"; } return $result; } public function setHandler(callable $handler) { $this->handler = $handler; $this->cached = null; } public function hasHandler() { return (bool) $this->handler; } public function unshift(callable $middleware, $name = null) { array_unshift($this->stack, [$middleware, $name]); $this->cached = null; } public function push(callable $middleware, $name = '') { $this->stack[] = [$middleware, $name]; $this->cached = null; } public function before($findName, callable $middleware, $withName = '') { $this->splice($findName, $withName, $middleware, true); } public function after($findName, callable $middleware, $withName = '') { $this->splice($findName, $withName, $middleware, false); } public function remove($remove) { $this->cached = null; $idx = is_callable($remove) ? 0 : 1; $this->stack = array_values(array_filter( $this->stack, function ($tuple) use ($idx, $remove) { return $tuple[$idx] !== $remove; } )); } public function resolve() { if (!$this->cached) { if (!($prev = $this->handler)) { throw new \LogicException('No handler has been specified'); } foreach (array_reverse($this->stack) as $fn) { $prev = $fn[0]($prev); } $this->cached = $prev; } return $this->cached; } private function findByName($name) { foreach ($this->stack as $k => $v) { if ($v[1] === $name) { return $k; } } throw new \InvalidArgumentException("Middleware not found: $name"); } private function splice($findName, $withName, callable $middleware, $before) { $this->cached = null; $idx = $this->findByName($findName); $tuple = [$middleware, $withName]; if ($before) { if ($idx === 0) { array_unshift($this->stack, $tuple); } else { $replacement = [$tuple, $this->stack[$idx]]; array_splice($this->stack, $idx, 1, $replacement); } } elseif ($idx === count($this->stack) - 1) { $this->stack[] = $tuple; } else { $replacement = [$this->stack[$idx], $tuple]; array_splice($this->stack, $idx, 1, $replacement); } } private function debugCallable($fn) { if (is_string($fn)) { return "callable({$fn})"; } if (is_array($fn)) { return is_string($fn[0]) ? "callable({$fn[0]}::{$fn[1]})" : "callable(['" . get_class($fn[0]) . "', '{$fn[1]}'])"; } return 'callable(' . spl_object_hash($fn) . ')'; } } >>>>>>>\n{request}\n<<<<<<<<\n{response}\n--------\n{error}"; const SHORT = '[{ts}] "{method} {target} HTTP/{version}" {code}'; private $template; public function __construct($template = self::CLF) { $this->template = $template ?: self::CLF; } public function format( RequestInterface $request, ResponseInterface $response = null, \Exception $error = null ) { $cache = []; return preg_replace_callback( '/{\s*([A-Za-z_\-\.0-9]+)\s*}/', function (array $matches) use ($request, $response, $error, &$cache) { if (isset($cache[$matches[1]])) { return $cache[$matches[1]]; } $result = ''; switch ($matches[1]) { case 'request': $result = Psr7\str($request); break; case 'response': $result = $response ? Psr7\str($response) : ''; break; case 'req_headers': $result = trim($request->getMethod() . ' ' . $request->getRequestTarget()) . ' HTTP/' . $request->getProtocolVersion() . "\r\n" . $this->headers($request); break; case 'res_headers': $result = $response ? sprintf( 'HTTP/%s %d %s', $response->getProtocolVersion(), $response->getStatusCode(), $response->getReasonPhrase() ) . "\r\n" . $this->headers($response) : 'NULL'; break; case 'req_body': $result = $request->getBody(); break; case 'res_body': $result = $response ? $response->getBody() : 'NULL'; break; case 'ts': case 'date_iso_8601': $result = gmdate('c'); break; case 'date_common_log': $result = date('d/M/Y:H:i:s O'); break; case 'method': $result = $request->getMethod(); break; case 'version': $result = $request->getProtocolVersion(); break; case 'uri': case 'url': $result = $request->getUri(); break; case 'target': $result = $request->getRequestTarget(); break; case 'req_version': $result = $request->getProtocolVersion(); break; case 'res_version': $result = $response ? $response->getProtocolVersion() : 'NULL'; break; case 'host': $result = $request->getHeaderLine('Host'); break; case 'hostname': $result = gethostname(); break; case 'code': $result = $response ? $response->getStatusCode() : 'NULL'; break; case 'phrase': $result = $response ? $response->getReasonPhrase() : 'NULL'; break; case 'error': $result = $error ? $error->getMessage() : 'NULL'; break; default: if (strpos($matches[1], 'req_header_') === 0) { $result = $request->getHeaderLine(substr($matches[1], 11)); } elseif (strpos($matches[1], 'res_header_') === 0) { $result = $response ? $response->getHeaderLine(substr($matches[1], 11)) : 'NULL'; } } $cache[$matches[1]] = $result; return $result; }, $this->template ); } private function headers(MessageInterface $message) { $result = ''; foreach ($message->getHeaders() as $name => $values) { $result .= $name . ': ' . implode(', ', $values) . "\r\n"; } return trim($result); } } withCookieHeader($request); return $handler($request, $options) ->then( function ($response) use ($cookieJar, $request) { $cookieJar->extractCookies($request, $response); return $response; } ); }; }; } public static function httpErrors() { return function (callable $handler) { return function ($request, array $options) use ($handler) { if (empty($options['http_errors'])) { return $handler($request, $options); } return $handler($request, $options)->then( function (ResponseInterface $response) use ($request, $handler) { $code = $response->getStatusCode(); if ($code < 400) { return $response; } throw RequestException::create($request, $response); } ); }; }; } public static function history(&$container) { if (!is_array($container) && !$container instanceof \ArrayAccess) { throw new \InvalidArgumentException('history container must be an array or object implementing ArrayAccess'); } return function (callable $handler) use (&$container) { return function ($request, array $options) use ($handler, &$container) { return $handler($request, $options)->then( function ($value) use ($request, &$container, $options) { $container[] = [ 'request' => $request, 'response' => $value, 'error' => null, 'options' => $options ]; return $value; }, function ($reason) use ($request, &$container, $options) { $container[] = [ 'request' => $request, 'response' => null, 'error' => $reason, 'options' => $options ]; return \GuzzleHttp\Promise\rejection_for($reason); } ); }; }; } public static function tap(callable $before = null, callable $after = null) { return function (callable $handler) use ($before, $after) { return function ($request, array $options) use ($handler, $before, $after) { if ($before) { $before($request, $options); } $response = $handler($request, $options); if ($after) { $after($request, $options, $response); } return $response; }; }; } public static function redirect() { return function (callable $handler) { return new RedirectMiddleware($handler); }; } public static function retry(callable $decider, callable $delay = null) { return function (callable $handler) use ($decider, $delay) { return new RetryMiddleware($decider, $handler, $delay); }; } public static function log(LoggerInterface $logger, MessageFormatter $formatter, $logLevel = LogLevel::INFO) { return function (callable $handler) use ($logger, $formatter, $logLevel) { return function ($request, array $options) use ($handler, $logger, $formatter, $logLevel) { return $handler($request, $options)->then( function ($response) use ($logger, $request, $formatter, $logLevel) { $message = $formatter->format($request, $response); $logger->log($logLevel, $message); return $response; }, function ($reason) use ($logger, $request, $formatter) { $response = $reason instanceof RequestException ? $reason->getResponse() : null; $message = $formatter->format($request, $response, $reason); $logger->notice($message); return \GuzzleHttp\Promise\rejection_for($reason); } ); }; }; } public static function prepareBody() { return function (callable $handler) { return new PrepareBodyMiddleware($handler); }; } public static function mapRequest(callable $fn) { return function (callable $handler) use ($fn) { return function ($request, array $options) use ($handler, $fn) { return $handler($fn($request), $options); }; }; } public static function mapResponse(callable $fn) { return function (callable $handler) use ($fn) { return function ($request, array $options) use ($handler, $fn) { return $handler($request, $options)->then($fn); }; }; } } $rfn) { if ($rfn instanceof RequestInterface) { yield $key => $client->sendAsync($rfn, $opts); } elseif (is_callable($rfn)) { yield $key => $rfn($opts); } else { throw new \InvalidArgumentException('Each value yielded by ' . 'the iterator must be a Psr7\Http\Message\RequestInterface ' . 'or a callable that returns a promise that fulfills ' . 'with a Psr7\Message\Http\ResponseInterface object.'); } } }; $this->each = new EachPromise($requests(), $config); } public function promise() { return $this->each->promise(); } public static function batch( ClientInterface $client, $requests, array $options = [] ) { $res = []; self::cmpCallback($options, 'fulfilled', $res); self::cmpCallback($options, 'rejected', $res); $pool = new static($client, $requests, $options); $pool->promise()->wait(); ksort($res); return $res; } private static function cmpCallback(array &$options, $name, array &$results) { if (!isset($options[$name])) { $options[$name] = function ($v, $k) use (&$results) { $results[$k] = $v; }; } else { $currentFn = $options[$name]; $options[$name] = function ($v, $k) use (&$results, $currentFn) { $currentFn($v, $k); $results[$k] = $v; }; } } } nextHandler = $nextHandler; } public function __invoke(RequestInterface $request, array $options) { $fn = $this->nextHandler; if ($request->getBody()->getSize() === 0) { return $fn($request, $options); } $modify = []; if (!$request->hasHeader('Content-Type')) { if ($uri = $request->getBody()->getMetadata('uri')) { if ($type = Psr7\mimetype_from_filename($uri)) { $modify['set_headers']['Content-Type'] = $type; } } } if (!$request->hasHeader('Content-Length') && !$request->hasHeader('Transfer-Encoding') ) { $size = $request->getBody()->getSize(); if ($size !== null) { $modify['set_headers']['Content-Length'] = $size; } else { $modify['set_headers']['Transfer-Encoding'] = 'chunked'; } } $this->addExpectHeader($request, $options, $modify); return $fn(Psr7\modify_request($request, $modify), $options); } private function addExpectHeader( RequestInterface $request, array $options, array &$modify ) { if ($request->hasHeader('Expect')) { return; } $expect = isset($options['expect']) ? $options['expect'] : null; if ($expect === false || $request->getProtocolVersion() < 1.1) { return; } if ($expect === true) { $modify['set_headers']['Expect'] = '100-Continue'; return; } if ($expect === null) { $expect = 1048576; } $body = $request->getBody(); $size = $body->getSize(); if ($size === null || $size >= (int) $expect || !$body->isSeekable()) { $modify['set_headers']['Expect'] = '100-Continue'; } } } 5, 'protocols' => ['http', 'https'], 'strict' => false, 'referer' => false, 'track_redirects' => false, ]; private $nextHandler; public function __construct(callable $nextHandler) { $this->nextHandler = $nextHandler; } public function __invoke(RequestInterface $request, array $options) { $fn = $this->nextHandler; if (empty($options['allow_redirects'])) { return $fn($request, $options); } if ($options['allow_redirects'] === true) { $options['allow_redirects'] = self::$defaultSettings; } elseif (!is_array($options['allow_redirects'])) { throw new \InvalidArgumentException('allow_redirects must be true, false, or array'); } else { $options['allow_redirects'] += self::$defaultSettings; } if (empty($options['allow_redirects']['max'])) { return $fn($request, $options); } return $fn($request, $options) ->then(function (ResponseInterface $response) use ($request, $options) { return $this->checkRedirect($request, $options, $response); }); } public function checkRedirect( RequestInterface $request, array $options, ResponseInterface $response ) { if (substr($response->getStatusCode(), 0, 1) != '3' || !$response->hasHeader('Location') ) { return $response; } $this->guardMax($request, $options); $nextRequest = $this->modifyRequest($request, $options, $response); if (isset($options['allow_redirects']['on_redirect'])) { call_user_func( $options['allow_redirects']['on_redirect'], $request, $response, $nextRequest->getUri() ); } $promise = $this($nextRequest, $options); if (!empty($options['allow_redirects']['track_redirects'])) { return $this->withTracking( $promise, (string) $nextRequest->getUri(), $response->getStatusCode() ); } return $promise; } private function withTracking(PromiseInterface $promise, $uri, $statusCode) { return $promise->then( function (ResponseInterface $response) use ($uri, $statusCode) { $historyHeader = $response->getHeader(self::HISTORY_HEADER); $statusHeader = $response->getHeader(self::STATUS_HISTORY_HEADER); array_unshift($historyHeader, $uri); array_unshift($statusHeader, $statusCode); return $response->withHeader(self::HISTORY_HEADER, $historyHeader) ->withHeader(self::STATUS_HISTORY_HEADER, $statusHeader); } ); } private function guardMax(RequestInterface $request, array &$options) { $current = isset($options['__redirect_count']) ? $options['__redirect_count'] : 0; $options['__redirect_count'] = $current + 1; $max = $options['allow_redirects']['max']; if ($options['__redirect_count'] > $max) { throw new TooManyRedirectsException( "Will not follow more than {$max} redirects", $request ); } } public function modifyRequest( RequestInterface $request, array $options, ResponseInterface $response ) { $modify = []; $protocols = $options['allow_redirects']['protocols']; $statusCode = $response->getStatusCode(); if ($statusCode == 303 || ($statusCode <= 302 && $request->getBody() && !$options['allow_redirects']['strict']) ) { $modify['method'] = 'GET'; $modify['body'] = ''; } $modify['uri'] = $this->redirectUri($request, $response, $protocols); Psr7\rewind_body($request); if ($options['allow_redirects']['referer'] && $modify['uri']->getScheme() === $request->getUri()->getScheme() ) { $uri = $request->getUri()->withUserInfo('', ''); $modify['set_headers']['Referer'] = (string) $uri; } else { $modify['remove_headers'][] = 'Referer'; } if ($request->getUri()->getHost() !== $modify['uri']->getHost()) { $modify['remove_headers'][] = 'Authorization'; } return Psr7\modify_request($request, $modify); } private function redirectUri( RequestInterface $request, ResponseInterface $response, array $protocols ) { $location = Psr7\UriResolver::resolve( $request->getUri(), new Psr7\Uri($response->getHeaderLine('Location')) ); if (!in_array($location->getScheme(), $protocols)) { throw new BadResponseException( sprintf( 'Redirect URI, %s, does not use one of the allowed redirect protocols: %s', $location, implode(', ', $protocols) ), $request, $response ); } return $location; } } decider = $decider; $this->nextHandler = $nextHandler; $this->delay = $delay ?: __CLASS__ . '::exponentialDelay'; } public static function exponentialDelay($retries) { return (int) pow(2, $retries - 1); } public function __invoke(RequestInterface $request, array $options) { if (!isset($options['retries'])) { $options['retries'] = 0; } $fn = $this->nextHandler; return $fn($request, $options) ->then( $this->onFulfilled($request, $options), $this->onRejected($request, $options) ); } private function onFulfilled(RequestInterface $req, array $options) { return function ($value) use ($req, $options) { if (!call_user_func( $this->decider, $options['retries'], $req, $value, null )) { return $value; } return $this->doRetry($req, $options, $value); }; } private function onRejected(RequestInterface $req, array $options) { return function ($reason) use ($req, $options) { if (!call_user_func( $this->decider, $options['retries'], $req, null, $reason )) { return \GuzzleHttp\Promise\rejection_for($reason); } return $this->doRetry($req, $options); }; } private function doRetry(RequestInterface $request, array $options, ResponseInterface $response = null) { $options['delay'] = call_user_func($this->delay, ++$options['retries'], $response); return $this($request, $options); } } request = $request; $this->response = $response; $this->transferTime = $transferTime; $this->handlerErrorData = $handlerErrorData; $this->handlerStats = $handlerStats; } public function getRequest() { return $this->request; } public function getResponse() { return $this->response; } public function hasResponse() { return $this->response !== null; } public function getHandlerErrorData() { return $this->handlerErrorData; } public function getEffectiveUri() { return $this->request->getUri(); } public function getTransferTime() { return $this->transferTime; } public function getHandlerStats() { return $this->handlerStats; } public function getHandlerStat($stat) { return isset($this->handlerStats[$stat]) ? $this->handlerStats[$stat] : null; } } ['prefix' => '', 'joiner' => ',', 'query' => false], '+' => ['prefix' => '', 'joiner' => ',', 'query' => false], '#' => ['prefix' => '#', 'joiner' => ',', 'query' => false], '.' => ['prefix' => '.', 'joiner' => '.', 'query' => false], '/' => ['prefix' => '/', 'joiner' => '/', 'query' => false], ';' => ['prefix' => ';', 'joiner' => ';', 'query' => true], '?' => ['prefix' => '?', 'joiner' => '&', 'query' => true], '&' => ['prefix' => '&', 'joiner' => '&', 'query' => true] ]; private static $delims = [':', '/', '?', '#', '[', ']', '@', '!', '$', '&', '\'', '(', ')', '*', '+', ',', ';', '=']; private static $delimsPct = ['%3A', '%2F', '%3F', '%23', '%5B', '%5D', '%40', '%21', '%24', '%26', '%27', '%28', '%29', '%2A', '%2B', '%2C', '%3B', '%3D']; public function expand($template, array $variables) { if (false === strpos($template, '{')) { return $template; } $this->template = $template; $this->variables = $variables; return preg_replace_callback( '/\{([^\}]+)\}/', [$this, 'expandMatch'], $this->template ); } private function parseExpression($expression) { $result = []; if (isset(self::$operatorHash[$expression[0]])) { $result['operator'] = $expression[0]; $expression = substr($expression, 1); } else { $result['operator'] = ''; } foreach (explode(',', $expression) as $value) { $value = trim($value); $varspec = []; if ($colonPos = strpos($value, ':')) { $varspec['value'] = substr($value, 0, $colonPos); $varspec['modifier'] = ':'; $varspec['position'] = (int) substr($value, $colonPos + 1); } elseif (substr($value, -1) === '*') { $varspec['modifier'] = '*'; $varspec['value'] = substr($value, 0, -1); } else { $varspec['value'] = (string) $value; $varspec['modifier'] = ''; } $result['values'][] = $varspec; } return $result; } private function expandMatch(array $matches) { static $rfc1738to3986 = ['+' => '%20', '%7e' => '~']; $replacements = []; $parsed = self::parseExpression($matches[1]); $prefix = self::$operatorHash[$parsed['operator']]['prefix']; $joiner = self::$operatorHash[$parsed['operator']]['joiner']; $useQuery = self::$operatorHash[$parsed['operator']]['query']; foreach ($parsed['values'] as $value) { if (!isset($this->variables[$value['value']])) { continue; } $variable = $this->variables[$value['value']]; $actuallyUseQuery = $useQuery; $expanded = ''; if (is_array($variable)) { $isAssoc = $this->isAssoc($variable); $kvp = []; foreach ($variable as $key => $var) { if ($isAssoc) { $key = rawurlencode($key); $isNestedArray = is_array($var); } else { $isNestedArray = false; } if (!$isNestedArray) { $var = rawurlencode($var); if ($parsed['operator'] === '+' || $parsed['operator'] === '#' ) { $var = $this->decodeReserved($var); } } if ($value['modifier'] === '*') { if ($isAssoc) { if ($isNestedArray) { $var = strtr( http_build_query([$key => $var]), $rfc1738to3986 ); } else { $var = $key . '=' . $var; } } elseif ($key > 0 && $actuallyUseQuery) { $var = $value['value'] . '=' . $var; } } $kvp[$key] = $var; } if (empty($variable)) { $actuallyUseQuery = false; } elseif ($value['modifier'] === '*') { $expanded = implode($joiner, $kvp); if ($isAssoc) { $actuallyUseQuery = false; } } else { if ($isAssoc) { foreach ($kvp as $k => &$v) { $v = $k . ',' . $v; } } $expanded = implode(',', $kvp); } } else { if ($value['modifier'] === ':') { $variable = substr($variable, 0, $value['position']); } $expanded = rawurlencode($variable); if ($parsed['operator'] === '+' || $parsed['operator'] === '#') { $expanded = $this->decodeReserved($expanded); } } if ($actuallyUseQuery) { if (!$expanded && $joiner !== '&') { $expanded = $value['value']; } else { $expanded = $value['value'] . '=' . $expanded; } } $replacements[] = $expanded; } $ret = implode($joiner, $replacements); if ($ret && $prefix) { return $prefix . $ret; } return $ret; } private function isAssoc(array $array) { return $array && array_keys($array)[0] !== 0; } private function decodeReserved($string) { return str_replace(self::$delimsPct, self::$delims, $string); } } generator = $generatorFn(); $this->result = new Promise(function () { while (isset($this->currentPromise)) { $this->currentPromise->wait(); } }); $this->nextCoroutine($this->generator->current()); } public function then( callable $onFulfilled = null, callable $onRejected = null ) { return $this->result->then($onFulfilled, $onRejected); } public function otherwise(callable $onRejected) { return $this->result->otherwise($onRejected); } public function wait($unwrap = true) { return $this->result->wait($unwrap); } public function getState() { return $this->result->getState(); } public function resolve($value) { $this->result->resolve($value); } public function reject($reason) { $this->result->reject($reason); } public function cancel() { $this->currentPromise->cancel(); $this->result->cancel(); } private function nextCoroutine($yielded) { $this->currentPromise = promise_for($yielded) ->then([$this, '_handleSuccess'], [$this, '_handleFailure']); } public function _handleSuccess($value) { unset($this->currentPromise); try { $next = $this->generator->send($value); if ($this->generator->valid()) { $this->nextCoroutine($next); } else { $this->result->resolve($value); } } catch (Exception $exception) { $this->result->reject($exception); } catch (Throwable $throwable) { $this->result->reject($throwable); } } public function _handleFailure($reason) { unset($this->currentPromise); try { $nextYield = $this->generator->throw(exception_for($reason)); $this->nextCoroutine($nextYield); } catch (Exception $exception) { $this->result->reject($exception); } catch (Throwable $throwable) { $this->result->reject($throwable); } } } iterable = iter_for($iterable); if (isset($config['concurrency'])) { $this->concurrency = $config['concurrency']; } if (isset($config['fulfilled'])) { $this->onFulfilled = $config['fulfilled']; } if (isset($config['rejected'])) { $this->onRejected = $config['rejected']; } } public function promise() { if ($this->aggregate) { return $this->aggregate; } try { $this->createPromise(); $this->iterable->rewind(); $this->refillPending(); } catch (\Throwable $e) { $this->aggregate->reject($e); } catch (\Exception $e) { $this->aggregate->reject($e); } return $this->aggregate; } private function createPromise() { $this->mutex = false; $this->aggregate = new Promise(function () { reset($this->pending); if (empty($this->pending) && !$this->iterable->valid()) { $this->aggregate->resolve(null); return; } while ($promise = current($this->pending)) { next($this->pending); $promise->wait(); if ($this->aggregate->getState() !== PromiseInterface::PENDING) { return; } } }); $clearFn = function () { $this->iterable = $this->concurrency = $this->pending = null; $this->onFulfilled = $this->onRejected = null; }; $this->aggregate->then($clearFn, $clearFn); } private function refillPending() { if (!$this->concurrency) { while ($this->addPending() && $this->advanceIterator()); return; } $concurrency = is_callable($this->concurrency) ? call_user_func($this->concurrency, count($this->pending)) : $this->concurrency; $concurrency = max($concurrency - count($this->pending), 0); if (!$concurrency) { return; } $this->addPending(); while (--$concurrency && $this->advanceIterator() && $this->addPending()); } private function addPending() { if (!$this->iterable || !$this->iterable->valid()) { return false; } $promise = promise_for($this->iterable->current()); $idx = $this->iterable->key(); $this->pending[$idx] = $promise->then( function ($value) use ($idx) { if ($this->onFulfilled) { call_user_func( $this->onFulfilled, $value, $idx, $this->aggregate ); } $this->step($idx); }, function ($reason) use ($idx) { if ($this->onRejected) { call_user_func( $this->onRejected, $reason, $idx, $this->aggregate ); } $this->step($idx); } ); return true; } private function advanceIterator() { if ($this->mutex) { return false; } $this->mutex = true; try { $this->iterable->next(); $this->mutex = false; return true; } catch (\Throwable $e) { $this->aggregate->reject($e); $this->mutex = false; return false; } catch (\Exception $e) { $this->aggregate->reject($e); $this->mutex = false; return false; } } private function step($idx) { if ($this->aggregate->getState() !== PromiseInterface::PENDING) { return; } unset($this->pending[$idx]); if ($this->advanceIterator() && !$this->checkIfFinished()) { $this->refillPending(); } } private function checkIfFinished() { if (!$this->pending && !$this->iterable->valid()) { $this->aggregate->resolve(null); return true; } return false; } } value = $value; } public function then( callable $onFulfilled = null, callable $onRejected = null ) { if (!$onFulfilled) { return $this; } $queue = queue(); $p = new Promise([$queue, 'run']); $value = $this->value; $queue->add(static function () use ($p, $value, $onFulfilled) { if ($p->getState() === self::PENDING) { try { $p->resolve($onFulfilled($value)); } catch (\Throwable $e) { $p->reject($e); } catch (\Exception $e) { $p->reject($e); } } }); return $p; } public function otherwise(callable $onRejected) { return $this->then(null, $onRejected); } public function wait($unwrap = true, $defaultDelivery = null) { return $unwrap ? $this->value : null; } public function getState() { return self::FULFILLED; } public function resolve($value) { if ($value !== $this->value) { throw new \LogicException("Cannot resolve a fulfilled promise"); } } public function reject($reason) { throw new \LogicException("Cannot reject a fulfilled promise"); } public function cancel() { } } add(function () use ($task, $promise) { try { $promise->resolve($task()); } catch (\Throwable $e) { $promise->reject($e); } catch (\Exception $e) { $promise->reject($e); } }); return $promise; } function promise_for($value) { if ($value instanceof PromiseInterface) { return $value; } if (method_exists($value, 'then')) { $wfn = method_exists($value, 'wait') ? [$value, 'wait'] : null; $cfn = method_exists($value, 'cancel') ? [$value, 'cancel'] : null; $promise = new Promise($wfn, $cfn); $value->then([$promise, 'resolve'], [$promise, 'reject']); return $promise; } return new FulfilledPromise($value); } function rejection_for($reason) { if ($reason instanceof PromiseInterface) { return $reason; } return new RejectedPromise($reason); } function exception_for($reason) { return $reason instanceof \Exception || $reason instanceof \Throwable ? $reason : new RejectionException($reason); } function iter_for($value) { if ($value instanceof \Iterator) { return $value; } elseif (is_array($value)) { return new \ArrayIterator($value); } else { return new \ArrayIterator([$value]); } } function inspect(PromiseInterface $promise) { try { return [ 'state' => PromiseInterface::FULFILLED, 'value' => $promise->wait() ]; } catch (RejectionException $e) { return ['state' => PromiseInterface::REJECTED, 'reason' => $e->getReason()]; } catch (\Throwable $e) { return ['state' => PromiseInterface::REJECTED, 'reason' => $e]; } catch (\Exception $e) { return ['state' => PromiseInterface::REJECTED, 'reason' => $e]; } } function inspect_all($promises) { $results = []; foreach ($promises as $key => $promise) { $results[$key] = inspect($promise); } return $results; } function unwrap($promises) { $results = []; foreach ($promises as $key => $promise) { $results[$key] = $promise->wait(); } return $results; } function all($promises) { $results = []; return each( $promises, function ($value, $idx) use (&$results) { $results[$idx] = $value; }, function ($reason, $idx, Promise $aggregate) { $aggregate->reject($reason); } )->then(function () use (&$results) { ksort($results); return $results; }); } function some($count, $promises) { $results = []; $rejections = []; return each( $promises, function ($value, $idx, PromiseInterface $p) use (&$results, $count) { if ($p->getState() !== PromiseInterface::PENDING) { return; } $results[$idx] = $value; if (count($results) >= $count) { $p->resolve(null); } }, function ($reason) use (&$rejections) { $rejections[] = $reason; } )->then( function () use (&$results, &$rejections, $count) { if (count($results) !== $count) { throw new AggregateException( 'Not enough promises to fulfill count', $rejections ); } ksort($results); return array_values($results); } ); } function any($promises) { return some(1, $promises)->then(function ($values) { return $values[0]; }); } function settle($promises) { $results = []; return each( $promises, function ($value, $idx) use (&$results) { $results[$idx] = ['state' => PromiseInterface::FULFILLED, 'value' => $value]; }, function ($reason, $idx) use (&$results) { $results[$idx] = ['state' => PromiseInterface::REJECTED, 'reason' => $reason]; } )->then(function () use (&$results) { ksort($results); return $results; }); } function each( $iterable, callable $onFulfilled = null, callable $onRejected = null ) { return (new EachPromise($iterable, [ 'fulfilled' => $onFulfilled, 'rejected' => $onRejected ]))->promise(); } function each_limit( $iterable, $concurrency, callable $onFulfilled = null, callable $onRejected = null ) { return (new EachPromise($iterable, [ 'fulfilled' => $onFulfilled, 'rejected' => $onRejected, 'concurrency' => $concurrency ]))->promise(); } function each_limit_all( $iterable, $concurrency, callable $onFulfilled = null ) { return each_limit( $iterable, $concurrency, $onFulfilled, function ($reason, $idx, PromiseInterface $aggregate) { $aggregate->reject($reason); } ); } function is_fulfilled(PromiseInterface $promise) { return $promise->getState() === PromiseInterface::FULFILLED; } function is_rejected(PromiseInterface $promise) { return $promise->getState() === PromiseInterface::REJECTED; } function is_settled(PromiseInterface $promise) { return $promise->getState() !== PromiseInterface::PENDING; } function coroutine(callable $generatorFn) { return new Coroutine($generatorFn); } waitFn = $waitFn; $this->cancelFn = $cancelFn; } public function then( callable $onFulfilled = null, callable $onRejected = null ) { if ($this->state === self::PENDING) { $p = new Promise(null, [$this, 'cancel']); $this->handlers[] = [$p, $onFulfilled, $onRejected]; $p->waitList = $this->waitList; $p->waitList[] = $this; return $p; } if ($this->state === self::FULFILLED) { return $onFulfilled ? promise_for($this->result)->then($onFulfilled) : promise_for($this->result); } $rejection = rejection_for($this->result); return $onRejected ? $rejection->then(null, $onRejected) : $rejection; } public function otherwise(callable $onRejected) { return $this->then(null, $onRejected); } public function wait($unwrap = true) { $this->waitIfPending(); $inner = $this->result instanceof PromiseInterface ? $this->result->wait($unwrap) : $this->result; if ($unwrap) { if ($this->result instanceof PromiseInterface || $this->state === self::FULFILLED ) { return $inner; } else { throw exception_for($inner); } } } public function getState() { return $this->state; } public function cancel() { if ($this->state !== self::PENDING) { return; } $this->waitFn = $this->waitList = null; if ($this->cancelFn) { $fn = $this->cancelFn; $this->cancelFn = null; try { $fn(); } catch (\Throwable $e) { $this->reject($e); } catch (\Exception $e) { $this->reject($e); } } if ($this->state === self::PENDING) { $this->reject(new CancellationException('Promise has been cancelled')); } } public function resolve($value) { $this->settle(self::FULFILLED, $value); } public function reject($reason) { $this->settle(self::REJECTED, $reason); } private function settle($state, $value) { if ($this->state !== self::PENDING) { if ($state === $this->state && $value === $this->result) { return; } throw $this->state === $state ? new \LogicException("The promise is already {$state}.") : new \LogicException("Cannot change a {$this->state} promise to {$state}"); } if ($value === $this) { throw new \LogicException('Cannot fulfill or reject a promise with itself'); } $this->state = $state; $this->result = $value; $handlers = $this->handlers; $this->handlers = null; $this->waitList = $this->waitFn = null; $this->cancelFn = null; if (!$handlers) { return; } if (!method_exists($value, 'then')) { $id = $state === self::FULFILLED ? 1 : 2; queue()->add(static function () use ($id, $value, $handlers) { foreach ($handlers as $handler) { self::callHandler($id, $value, $handler); } }); } elseif ($value instanceof Promise && $value->getState() === self::PENDING ) { $value->handlers = array_merge($value->handlers, $handlers); } else { $value->then( static function ($value) use ($handlers) { foreach ($handlers as $handler) { self::callHandler(1, $value, $handler); } }, static function ($reason) use ($handlers) { foreach ($handlers as $handler) { self::callHandler(2, $reason, $handler); } } ); } } private static function callHandler($index, $value, array $handler) { $promise = $handler[0]; if ($promise->getState() !== self::PENDING) { return; } try { if (isset($handler[$index])) { $promise->resolve($handler[$index]($value)); } elseif ($index === 1) { $promise->resolve($value); } else { $promise->reject($value); } } catch (\Throwable $reason) { $promise->reject($reason); } catch (\Exception $reason) { $promise->reject($reason); } } private function waitIfPending() { if ($this->state !== self::PENDING) { return; } elseif ($this->waitFn) { $this->invokeWaitFn(); } elseif ($this->waitList) { $this->invokeWaitList(); } else { $this->reject('Cannot wait on a promise that has ' . 'no internal wait function. You must provide a wait ' . 'function when constructing the promise to be able to ' . 'wait on a promise.'); } queue()->run(); if ($this->state === self::PENDING) { $this->reject('Invoking the wait callback did not resolve the promise'); } } private function invokeWaitFn() { try { $wfn = $this->waitFn; $this->waitFn = null; $wfn(true); } catch (\Exception $reason) { if ($this->state === self::PENDING) { $this->reject($reason); } else { throw $reason; } } } private function invokeWaitList() { $waitList = $this->waitList; $this->waitList = null; foreach ($waitList as $result) { while (true) { $result->waitIfPending(); if ($result->result instanceof Promise) { $result = $result->result; } else { if ($result->result instanceof PromiseInterface) { $result->result->wait(false); } break; } } } } } reason = $reason; } public function then( callable $onFulfilled = null, callable $onRejected = null ) { if (!$onRejected) { return $this; } $queue = queue(); $reason = $this->reason; $p = new Promise([$queue, 'run']); $queue->add(static function () use ($p, $reason, $onRejected) { if ($p->getState() === self::PENDING) { try { $p->resolve($onRejected($reason)); } catch (\Throwable $e) { $p->reject($e); } catch (\Exception $e) { $p->reject($e); } } }); return $p; } public function otherwise(callable $onRejected) { return $this->then(null, $onRejected); } public function wait($unwrap = true, $defaultDelivery = null) { if ($unwrap) { throw exception_for($this->reason); } } public function getState() { return self::REJECTED; } public function resolve($value) { throw new \LogicException("Cannot resolve a rejected promise"); } public function reject($reason) { if ($reason !== $this->reason) { throw new \LogicException("Cannot reject a rejected promise"); } } public function cancel() { } } reason = $reason; $message = 'The promise was rejected'; if ($description) { $message .= ' with reason: ' . $description; } elseif (is_string($reason) || (is_object($reason) && method_exists($reason, '__toString')) ) { $message .= ' with reason: ' . $this->reason; } elseif ($reason instanceof \JsonSerializable) { $message .= ' with reason: ' . json_encode($this->reason, JSON_PRETTY_PRINT); } parent::__construct($message); } public function getReason() { return $this->reason; } } enableShutdown) { $err = error_get_last(); if (!$err || ($err['type'] ^ E_ERROR)) { $this->run(); } } }); } } public function isEmpty() { return !$this->queue; } public function add(callable $task) { $this->queue[] = $task; } public function run() { while ($task = array_shift($this->queue)) { $task(); } } public function disableShutdown() { $this->enableShutdown = false; } } addStream($stream); } } public function __toString() { try { $this->rewind(); return $this->getContents(); } catch (\Exception $e) { return ''; } } public function addStream(StreamInterface $stream) { if (!$stream->isReadable()) { throw new \InvalidArgumentException('Each stream must be readable'); } if (!$stream->isSeekable()) { $this->seekable = false; } $this->streams[] = $stream; } public function getContents() { return copy_to_string($this); } public function close() { $this->pos = $this->current = 0; $this->seekable = true; foreach ($this->streams as $stream) { $stream->close(); } $this->streams = []; } public function detach() { $this->pos = $this->current = 0; $this->seekable = true; foreach ($this->streams as $stream) { $stream->detach(); } $this->streams = []; } public function tell() { return $this->pos; } public function getSize() { $size = 0; foreach ($this->streams as $stream) { $s = $stream->getSize(); if ($s === null) { return null; } $size += $s; } return $size; } public function eof() { return !$this->streams || ($this->current >= count($this->streams) - 1 && $this->streams[$this->current]->eof()); } public function rewind() { $this->seek(0); } public function seek($offset, $whence = SEEK_SET) { if (!$this->seekable) { throw new \RuntimeException('This AppendStream is not seekable'); } elseif ($whence !== SEEK_SET) { throw new \RuntimeException('The AppendStream can only seek with SEEK_SET'); } $this->pos = $this->current = 0; foreach ($this->streams as $i => $stream) { try { $stream->rewind(); } catch (\Exception $e) { throw new \RuntimeException('Unable to seek stream ' . $i . ' of the AppendStream', 0, $e); } } while ($this->pos < $offset && !$this->eof()) { $result = $this->read(min(8096, $offset - $this->pos)); if ($result === '') { break; } } } public function read($length) { $buffer = ''; $total = count($this->streams) - 1; $remaining = $length; $progressToNext = false; while ($remaining > 0) { if ($progressToNext || $this->streams[$this->current]->eof()) { $progressToNext = false; if ($this->current === $total) { break; } $this->current++; } $result = $this->streams[$this->current]->read($remaining); if ($result == null) { $progressToNext = true; continue; } $buffer .= $result; $remaining = $length - strlen($buffer); } $this->pos += strlen($buffer); return $buffer; } public function isReadable() { return true; } public function isWritable() { return false; } public function isSeekable() { return $this->seekable; } public function write($string) { throw new \RuntimeException('Cannot write to an AppendStream'); } public function getMetadata($key = null) { return $key ? null : []; } } hwm = $hwm; } public function __toString() { return $this->getContents(); } public function getContents() { $buffer = $this->buffer; $this->buffer = ''; return $buffer; } public function close() { $this->buffer = ''; } public function detach() { $this->close(); } public function getSize() { return strlen($this->buffer); } public function isReadable() { return true; } public function isWritable() { return true; } public function isSeekable() { return false; } public function rewind() { $this->seek(0); } public function seek($offset, $whence = SEEK_SET) { throw new \RuntimeException('Cannot seek a BufferStream'); } public function eof() { return strlen($this->buffer) === 0; } public function tell() { throw new \RuntimeException('Cannot determine the position of a BufferStream'); } public function read($length) { $currentLength = strlen($this->buffer); if ($length >= $currentLength) { $result = $this->buffer; $this->buffer = ''; } else { $result = substr($this->buffer, 0, $length); $this->buffer = substr($this->buffer, $length); } return $result; } public function write($string) { $this->buffer .= $string; if (strlen($this->buffer) >= $this->hwm) { return false; } return strlen($string); } public function getMetadata($key = null) { if ($key == 'hwm') { return $this->hwm; } return $key ? null : []; } } remoteStream = $stream; $this->stream = $target ?: new Stream(fopen('php://temp', 'r+')); } public function getSize() { return max($this->stream->getSize(), $this->remoteStream->getSize()); } public function rewind() { $this->seek(0); } public function seek($offset, $whence = SEEK_SET) { if ($whence == SEEK_SET) { $byte = $offset; } elseif ($whence == SEEK_CUR) { $byte = $offset + $this->tell(); } elseif ($whence == SEEK_END) { $size = $this->remoteStream->getSize(); if ($size === null) { $size = $this->cacheEntireStream(); } $byte = $size + $offset; } else { throw new \InvalidArgumentException('Invalid whence'); } $diff = $byte - $this->stream->getSize(); if ($diff > 0) { while ($diff > 0 && !$this->remoteStream->eof()) { $this->read($diff); $diff = $byte - $this->stream->getSize(); } } else { $this->stream->seek($byte); } } public function read($length) { $data = $this->stream->read($length); $remaining = $length - strlen($data); if ($remaining) { $remoteData = $this->remoteStream->read( $remaining + $this->skipReadBytes ); if ($this->skipReadBytes) { $len = strlen($remoteData); $remoteData = substr($remoteData, $this->skipReadBytes); $this->skipReadBytes = max(0, $this->skipReadBytes - $len); } $data .= $remoteData; $this->stream->write($remoteData); } return $data; } public function write($string) { $overflow = (strlen($string) + $this->tell()) - $this->remoteStream->tell(); if ($overflow > 0) { $this->skipReadBytes += $overflow; } return $this->stream->write($string); } public function eof() { return $this->stream->eof() && $this->remoteStream->eof(); } public function close() { $this->remoteStream->close() && $this->stream->close(); } private function cacheEntireStream() { $target = new FnStream(['write' => 'strlen']); copy_to_stream($this, $target); return $this->tell(); } } stream = $stream; $this->maxLength = $maxLength; } public function write($string) { $diff = $this->maxLength - $this->stream->getSize(); if ($diff <= 0) { return 0; } if (strlen($string) < $diff) { return $this->stream->write($string); } return $this->stream->write(substr($string, 0, $diff)); } } methods = $methods; foreach ($methods as $name => $fn) { $this->{'_fn_' . $name} = $fn; } } public function __get($name) { throw new \BadMethodCallException(str_replace('_fn_', '', $name) . '() is not implemented in the FnStream'); } public function __destruct() { if (isset($this->_fn_close)) { call_user_func($this->_fn_close); } } public function __wakeup() { throw new \LogicException('FnStream should never be unserialized'); } public static function decorate(StreamInterface $stream, array $methods) { foreach (array_diff(self::$slots, array_keys($methods)) as $diff) { $methods[$diff] = [$stream, $diff]; } return new self($methods); } public function __toString() { return call_user_func($this->_fn___toString); } public function close() { return call_user_func($this->_fn_close); } public function detach() { return call_user_func($this->_fn_detach); } public function getSize() { return call_user_func($this->_fn_getSize); } public function tell() { return call_user_func($this->_fn_tell); } public function eof() { return call_user_func($this->_fn_eof); } public function isSeekable() { return call_user_func($this->_fn_isSeekable); } public function rewind() { call_user_func($this->_fn_rewind); } public function seek($offset, $whence = SEEK_SET) { call_user_func($this->_fn_seek, $offset, $whence); } public function isWritable() { return call_user_func($this->_fn_isWritable); } public function write($string) { return call_user_func($this->_fn_write, $string); } public function isReadable() { return call_user_func($this->_fn_isReadable); } public function read($length) { return call_user_func($this->_fn_read, $length); } public function getContents() { return call_user_func($this->_fn_getContents); } public function getMetadata($key = null) { return call_user_func($this->_fn_getMetadata, $key); } } getMethod() . ' ' . $message->getRequestTarget()) . ' HTTP/' . $message->getProtocolVersion(); if (!$message->hasHeader('host')) { $msg .= "\r\nHost: " . $message->getUri()->getHost(); } } elseif ($message instanceof ResponseInterface) { $msg = 'HTTP/' . $message->getProtocolVersion() . ' ' . $message->getStatusCode() . ' ' . $message->getReasonPhrase(); } else { throw new \InvalidArgumentException('Unknown message type'); } foreach ($message->getHeaders() as $name => $values) { $msg .= "\r\n{$name}: " . implode(', ', $values); } return "{$msg}\r\n\r\n" . $message->getBody(); } function uri_for($uri) { if ($uri instanceof UriInterface) { return $uri; } elseif (is_string($uri)) { return new Uri($uri); } throw new \InvalidArgumentException('URI must be a string or UriInterface'); } function stream_for($resource = '', array $options = []) { if (is_scalar($resource)) { $stream = fopen('php://temp', 'r+'); if ($resource !== '') { fwrite($stream, $resource); fseek($stream, 0); } return new Stream($stream, $options); } switch (gettype($resource)) { case 'resource': return new Stream($resource, $options); case 'object': if ($resource instanceof StreamInterface) { return $resource; } elseif ($resource instanceof \Iterator) { return new PumpStream(function () use ($resource) { if (!$resource->valid()) { return false; } $result = $resource->current(); $resource->next(); return $result; }, $options); } elseif (method_exists($resource, '__toString')) { return stream_for((string) $resource, $options); } break; case 'NULL': return new Stream(fopen('php://temp', 'r+'), $options); } if (is_callable($resource)) { return new PumpStream($resource, $options); } throw new \InvalidArgumentException('Invalid resource type: ' . gettype($resource)); } function parse_header($header) { static $trimmed = "\"' \n\t\r"; $params = $matches = []; foreach (normalize_header($header) as $val) { $part = []; foreach (preg_split('/;(?=([^"]*"[^"]*")*[^"]*$)/', $val) as $kvp) { if (preg_match_all('/<[^>]+>|[^=]+/', $kvp, $matches)) { $m = $matches[0]; if (isset($m[1])) { $part[trim($m[0], $trimmed)] = trim($m[1], $trimmed); } else { $part[] = trim($m[0], $trimmed); } } } if ($part) { $params[] = $part; } } return $params; } function normalize_header($header) { if (!is_array($header)) { return array_map('trim', explode(',', $header)); } $result = []; foreach ($header as $value) { foreach ((array) $value as $v) { if (strpos($v, ',') === false) { $result[] = $v; continue; } foreach (preg_split('/,(?=([^"]*"[^"]*")*[^"]*$)/', $v) as $vv) { $result[] = trim($vv); } } } return $result; } function modify_request(RequestInterface $request, array $changes) { if (!$changes) { return $request; } $headers = $request->getHeaders(); if (!isset($changes['uri'])) { $uri = $request->getUri(); } else { if ($host = $changes['uri']->getHost()) { $changes['set_headers']['Host'] = $host; if ($port = $changes['uri']->getPort()) { $standardPorts = ['http' => 80, 'https' => 443]; $scheme = $changes['uri']->getScheme(); if (isset($standardPorts[$scheme]) && $port != $standardPorts[$scheme]) { $changes['set_headers']['Host'] .= ':'.$port; } } } $uri = $changes['uri']; } if (!empty($changes['remove_headers'])) { $headers = _caseless_remove($changes['remove_headers'], $headers); } if (!empty($changes['set_headers'])) { $headers = _caseless_remove(array_keys($changes['set_headers']), $headers); $headers = $changes['set_headers'] + $headers; } if (isset($changes['query'])) { $uri = $uri->withQuery($changes['query']); } if ($request instanceof ServerRequestInterface) { return (new ServerRequest( isset($changes['method']) ? $changes['method'] : $request->getMethod(), $uri, $headers, isset($changes['body']) ? $changes['body'] : $request->getBody(), isset($changes['version']) ? $changes['version'] : $request->getProtocolVersion(), $request->getServerParams() )) ->withParsedBody($request->getParsedBody()) ->withQueryParams($request->getQueryParams()) ->withCookieParams($request->getCookieParams()) ->withUploadedFiles($request->getUploadedFiles()); } return new Request( isset($changes['method']) ? $changes['method'] : $request->getMethod(), $uri, $headers, isset($changes['body']) ? $changes['body'] : $request->getBody(), isset($changes['version']) ? $changes['version'] : $request->getProtocolVersion() ); } function rewind_body(MessageInterface $message) { $body = $message->getBody(); if ($body->tell()) { $body->rewind(); } } function try_fopen($filename, $mode) { $ex = null; set_error_handler(function () use ($filename, $mode, &$ex) { $ex = new \RuntimeException(sprintf( 'Unable to open %s using mode %s: %s', $filename, $mode, func_get_args()[1] )); }); $handle = fopen($filename, $mode); restore_error_handler(); if ($ex) { throw $ex; } return $handle; } function copy_to_string(StreamInterface $stream, $maxLen = -1) { $buffer = ''; if ($maxLen === -1) { while (!$stream->eof()) { $buf = $stream->read(1048576); if ($buf == null) { break; } $buffer .= $buf; } return $buffer; } $len = 0; while (!$stream->eof() && $len < $maxLen) { $buf = $stream->read($maxLen - $len); if ($buf == null) { break; } $buffer .= $buf; $len = strlen($buffer); } return $buffer; } function copy_to_stream( StreamInterface $source, StreamInterface $dest, $maxLen = -1 ) { $bufferSize = 8192; if ($maxLen === -1) { while (!$source->eof()) { if (!$dest->write($source->read($bufferSize))) { break; } } } else { $remaining = $maxLen; while ($remaining > 0 && !$source->eof()) { $buf = $source->read(min($bufferSize, $remaining)); $len = strlen($buf); if (!$len) { break; } $remaining -= $len; $dest->write($buf); } } } function hash( StreamInterface $stream, $algo, $rawOutput = false ) { $pos = $stream->tell(); if ($pos > 0) { $stream->rewind(); } $ctx = hash_init($algo); while (!$stream->eof()) { hash_update($ctx, $stream->read(1048576)); } $out = hash_final($ctx, (bool) $rawOutput); $stream->seek($pos); return $out; } function readline(StreamInterface $stream, $maxLength = null) { $buffer = ''; $size = 0; while (!$stream->eof()) { if (null == ($byte = $stream->read(1))) { return $buffer; } $buffer .= $byte; if ($byte === "\n" || ++$size === $maxLength - 1) { break; } } return $buffer; } function parse_request($message) { $data = _parse_message($message); $matches = []; if (!preg_match('/^[\S]+\s+([a-zA-Z]+:\/\/|\/).*/', $data['start-line'], $matches)) { throw new \InvalidArgumentException('Invalid request string'); } $parts = explode(' ', $data['start-line'], 3); $version = isset($parts[2]) ? explode('/', $parts[2])[1] : '1.1'; $request = new Request( $parts[0], $matches[1] === '/' ? _parse_request_uri($parts[1], $data['headers']) : $parts[1], $data['headers'], $data['body'], $version ); return $matches[1] === '/' ? $request : $request->withRequestTarget($parts[1]); } function parse_response($message) { $data = _parse_message($message); if (!preg_match('/^HTTP\/.* [0-9]{3}( .*|$)/', $data['start-line'])) { throw new \InvalidArgumentException('Invalid response string: ' . $data['start-line']); } $parts = explode(' ', $data['start-line'], 3); return new Response( $parts[1], $data['headers'], $data['body'], explode('/', $parts[0])[1], isset($parts[2]) ? $parts[2] : null ); } function parse_query($str, $urlEncoding = true) { $result = []; if ($str === '') { return $result; } if ($urlEncoding === true) { $decoder = function ($value) { return rawurldecode(str_replace('+', ' ', $value)); }; } elseif ($urlEncoding === PHP_QUERY_RFC3986) { $decoder = 'rawurldecode'; } elseif ($urlEncoding === PHP_QUERY_RFC1738) { $decoder = 'urldecode'; } else { $decoder = function ($str) { return $str; }; } foreach (explode('&', $str) as $kvp) { $parts = explode('=', $kvp, 2); $key = $decoder($parts[0]); $value = isset($parts[1]) ? $decoder($parts[1]) : null; if (!isset($result[$key])) { $result[$key] = $value; } else { if (!is_array($result[$key])) { $result[$key] = [$result[$key]]; } $result[$key][] = $value; } } return $result; } function build_query(array $params, $encoding = PHP_QUERY_RFC3986) { if (!$params) { return ''; } if ($encoding === false) { $encoder = function ($str) { return $str; }; } elseif ($encoding === PHP_QUERY_RFC3986) { $encoder = 'rawurlencode'; } elseif ($encoding === PHP_QUERY_RFC1738) { $encoder = 'urlencode'; } else { throw new \InvalidArgumentException('Invalid type'); } $qs = ''; foreach ($params as $k => $v) { $k = $encoder($k); if (!is_array($v)) { $qs .= $k; if ($v !== null) { $qs .= '=' . $encoder($v); } $qs .= '&'; } else { foreach ($v as $vv) { $qs .= $k; if ($vv !== null) { $qs .= '=' . $encoder($vv); } $qs .= '&'; } } } return $qs ? (string) substr($qs, 0, -1) : ''; } function mimetype_from_filename($filename) { return mimetype_from_extension(pathinfo($filename, PATHINFO_EXTENSION)); } function mimetype_from_extension($extension) { static $mimetypes = [ '3gp' => 'video/3gpp', '7z' => 'application/x-7z-compressed', 'aac' => 'audio/x-aac', 'ai' => 'application/postscript', 'aif' => 'audio/x-aiff', 'asc' => 'text/plain', 'asf' => 'video/x-ms-asf', 'atom' => 'application/atom+xml', 'avi' => 'video/x-msvideo', 'bmp' => 'image/bmp', 'bz2' => 'application/x-bzip2', 'cer' => 'application/pkix-cert', 'crl' => 'application/pkix-crl', 'crt' => 'application/x-x509-ca-cert', 'css' => 'text/css', 'csv' => 'text/csv', 'cu' => 'application/cu-seeme', 'deb' => 'application/x-debian-package', 'doc' => 'application/msword', 'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'dvi' => 'application/x-dvi', 'eot' => 'application/vnd.ms-fontobject', 'eps' => 'application/postscript', 'epub' => 'application/epub+zip', 'etx' => 'text/x-setext', 'flac' => 'audio/flac', 'flv' => 'video/x-flv', 'gif' => 'image/gif', 'gz' => 'application/gzip', 'htm' => 'text/html', 'html' => 'text/html', 'ico' => 'image/x-icon', 'ics' => 'text/calendar', 'ini' => 'text/plain', 'iso' => 'application/x-iso9660-image', 'jar' => 'application/java-archive', 'jpe' => 'image/jpeg', 'jpeg' => 'image/jpeg', 'jpg' => 'image/jpeg', 'js' => 'text/javascript', 'json' => 'application/json', 'latex' => 'application/x-latex', 'log' => 'text/plain', 'm4a' => 'audio/mp4', 'm4v' => 'video/mp4', 'mid' => 'audio/midi', 'midi' => 'audio/midi', 'mov' => 'video/quicktime', 'mkv' => 'video/x-matroska', 'mp3' => 'audio/mpeg', 'mp4' => 'video/mp4', 'mp4a' => 'audio/mp4', 'mp4v' => 'video/mp4', 'mpe' => 'video/mpeg', 'mpeg' => 'video/mpeg', 'mpg' => 'video/mpeg', 'mpg4' => 'video/mp4', 'oga' => 'audio/ogg', 'ogg' => 'audio/ogg', 'ogv' => 'video/ogg', 'ogx' => 'application/ogg', 'pbm' => 'image/x-portable-bitmap', 'pdf' => 'application/pdf', 'pgm' => 'image/x-portable-graymap', 'png' => 'image/png', 'pnm' => 'image/x-portable-anymap', 'ppm' => 'image/x-portable-pixmap', 'ppt' => 'application/vnd.ms-powerpoint', 'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation', 'ps' => 'application/postscript', 'qt' => 'video/quicktime', 'rar' => 'application/x-rar-compressed', 'ras' => 'image/x-cmu-raster', 'rss' => 'application/rss+xml', 'rtf' => 'application/rtf', 'sgm' => 'text/sgml', 'sgml' => 'text/sgml', 'svg' => 'image/svg+xml', 'swf' => 'application/x-shockwave-flash', 'tar' => 'application/x-tar', 'tif' => 'image/tiff', 'tiff' => 'image/tiff', 'torrent' => 'application/x-bittorrent', 'ttf' => 'application/x-font-ttf', 'txt' => 'text/plain', 'wav' => 'audio/x-wav', 'webm' => 'video/webm', 'wma' => 'audio/x-ms-wma', 'wmv' => 'video/x-ms-wmv', 'woff' => 'application/x-font-woff', 'wsdl' => 'application/wsdl+xml', 'xbm' => 'image/x-xbitmap', 'xls' => 'application/vnd.ms-excel', 'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'xml' => 'application/xml', 'xpm' => 'image/x-xpixmap', 'xwd' => 'image/x-xwindowdump', 'yaml' => 'text/yaml', 'yml' => 'text/yaml', 'zip' => 'application/zip', ]; $extension = strtolower($extension); return isset($mimetypes[$extension]) ? $mimetypes[$extension] : null; } function _parse_message($message) { if (!$message) { throw new \InvalidArgumentException('Invalid message'); } $message = ltrim($message, "\r\n"); $messageParts = preg_split("/\r?\n\r?\n/", $message, 2); if ($messageParts === false || count($messageParts) !== 2) { throw new \InvalidArgumentException('Invalid message: Missing header delimiter'); } list($rawHeaders, $body) = $messageParts; $rawHeaders .= "\r\n"; $headerParts = preg_split("/\r?\n/", $rawHeaders, 2); if ($headerParts === false || count($headerParts) !== 2) { throw new \InvalidArgumentException('Invalid message: Missing status line'); } list($startLine, $rawHeaders) = $headerParts; if (preg_match("/(?:^HTTP\/|^[A-Z]+ \S+ HTTP\/)(\d+(?:\.\d+)?)/i", $startLine, $matches) && $matches[1] === '1.0') { $rawHeaders = preg_replace(Rfc7230::HEADER_FOLD_REGEX, ' ', $rawHeaders); } $count = preg_match_all(Rfc7230::HEADER_REGEX, $rawHeaders, $headerLines, PREG_SET_ORDER); if ($count !== substr_count($rawHeaders, "\n")) { if (preg_match(Rfc7230::HEADER_FOLD_REGEX, $rawHeaders)) { throw new \InvalidArgumentException('Invalid header syntax: Obsolete line folding'); } throw new \InvalidArgumentException('Invalid header syntax'); } $headers = []; foreach ($headerLines as $headerLine) { $headers[$headerLine[1]][] = $headerLine[2]; } return [ 'start-line' => $startLine, 'headers' => $headers, 'body' => $body, ]; } function _parse_request_uri($path, array $headers) { $hostKey = array_filter(array_keys($headers), function ($k) { return strtolower($k) === 'host'; }); if (!$hostKey) { return $path; } $host = $headers[reset($hostKey)][0]; $scheme = substr($host, -4) === ':443' ? 'https' : 'http'; return $scheme . '://' . $host . '/' . ltrim($path, '/'); } function get_message_body_summary(MessageInterface $message, $truncateAt = 120) { $body = $message->getBody(); if (!$body->isSeekable() || !$body->isReadable()) { return null; } $size = $body->getSize(); if ($size === 0) { return null; } $summary = $body->read($truncateAt); $body->rewind(); if ($size > $truncateAt) { $summary .= ' (truncated...)'; } if (preg_match('/[^\pL\pM\pN\pP\pS\pZ\n\r\t]/', $summary)) { return null; } return $summary; } function _caseless_remove($keys, array $data) { $result = []; foreach ($keys as &$key) { $key = strtolower($key); } foreach ($data as $k => $v) { if (!in_array(strtolower($k), $keys)) { $result[$k] = $v; } } return $result; } read(10); $filenameHeaderLength = $this->getLengthOfPossibleFilenameHeader($stream, $header); $stream = new LimitStream($stream, -1, 10 + $filenameHeaderLength); $resource = StreamWrapper::getResource($stream); stream_filter_append($resource, 'zlib.inflate', STREAM_FILTER_READ); $this->stream = $stream->isSeekable() ? new Stream($resource) : new NoSeekStream(new Stream($resource)); } private function getLengthOfPossibleFilenameHeader(StreamInterface $stream, $header) { $filename_header_length = 0; if (substr(bin2hex($header), 6, 2) === '08') { $filename_header_length = 1; while ($stream->read(1) !== chr(0)) { $filename_header_length++; } } return $filename_header_length; } } filename = $filename; $this->mode = $mode; } protected function createStream() { return stream_for(try_fopen($this->filename, $this->mode)); } } stream = $stream; $this->setLimit($limit); $this->setOffset($offset); } public function eof() { if ($this->stream->eof()) { return true; } if ($this->limit == -1) { return false; } return $this->stream->tell() >= $this->offset + $this->limit; } public function getSize() { if (null === ($length = $this->stream->getSize())) { return null; } elseif ($this->limit == -1) { return $length - $this->offset; } else { return min($this->limit, $length - $this->offset); } } public function seek($offset, $whence = SEEK_SET) { if ($whence !== SEEK_SET || $offset < 0) { throw new \RuntimeException(sprintf( 'Cannot seek to offset % with whence %s', $offset, $whence )); } $offset += $this->offset; if ($this->limit !== -1) { if ($offset > $this->offset + $this->limit) { $offset = $this->offset + $this->limit; } } $this->stream->seek($offset); } public function tell() { return $this->stream->tell() - $this->offset; } public function setOffset($offset) { $current = $this->stream->tell(); if ($current !== $offset) { if ($this->stream->isSeekable()) { $this->stream->seek($offset); } elseif ($current > $offset) { throw new \RuntimeException("Could not seek to stream offset $offset"); } else { $this->stream->read($offset - $current); } } $this->offset = $offset; } public function setLimit($limit) { $this->limit = $limit; } public function read($length) { if ($this->limit == -1) { return $this->stream->read($length); } $remaining = ($this->offset + $this->limit) - $this->stream->tell(); if ($remaining > 0) { return $this->stream->read(min($remaining, $length)); } return ''; } } protocol; } public function withProtocolVersion($version) { if ($this->protocol === $version) { return $this; } $new = clone $this; $new->protocol = $version; return $new; } public function getHeaders() { return $this->headers; } public function hasHeader($header) { return isset($this->headerNames[strtolower($header)]); } public function getHeader($header) { $header = strtolower($header); if (!isset($this->headerNames[$header])) { return []; } $header = $this->headerNames[$header]; return $this->headers[$header]; } public function getHeaderLine($header) { return implode(', ', $this->getHeader($header)); } public function withHeader($header, $value) { if (!is_array($value)) { $value = [$value]; } $value = $this->trimHeaderValues($value); $normalized = strtolower($header); $new = clone $this; if (isset($new->headerNames[$normalized])) { unset($new->headers[$new->headerNames[$normalized]]); } $new->headerNames[$normalized] = $header; $new->headers[$header] = $value; return $new; } public function withAddedHeader($header, $value) { if (!is_array($value)) { $value = [$value]; } $value = $this->trimHeaderValues($value); $normalized = strtolower($header); $new = clone $this; if (isset($new->headerNames[$normalized])) { $header = $this->headerNames[$normalized]; $new->headers[$header] = array_merge($this->headers[$header], $value); } else { $new->headerNames[$normalized] = $header; $new->headers[$header] = $value; } return $new; } public function withoutHeader($header) { $normalized = strtolower($header); if (!isset($this->headerNames[$normalized])) { return $this; } $header = $this->headerNames[$normalized]; $new = clone $this; unset($new->headers[$header], $new->headerNames[$normalized]); return $new; } public function getBody() { if (!$this->stream) { $this->stream = stream_for(''); } return $this->stream; } public function withBody(StreamInterface $body) { if ($body === $this->stream) { return $this; } $new = clone $this; $new->stream = $body; return $new; } private function setHeaders(array $headers) { $this->headerNames = $this->headers = []; foreach ($headers as $header => $value) { if (!is_array($value)) { $value = [$value]; } $value = $this->trimHeaderValues($value); $normalized = strtolower($header); if (isset($this->headerNames[$normalized])) { $header = $this->headerNames[$normalized]; $this->headers[$header] = array_merge($this->headers[$header], $value); } else { $this->headerNames[$normalized] = $header; $this->headers[$header] = $value; } } } private function trimHeaderValues(array $values) { return array_map(function ($value) { return trim($value, " \t"); }, $values); } } boundary = $boundary ?: sha1(uniqid('', true)); $this->stream = $this->createStream($elements); } public function getBoundary() { return $this->boundary; } public function isWritable() { return false; } private function getHeaders(array $headers) { $str = ''; foreach ($headers as $key => $value) { $str .= "{$key}: {$value}\r\n"; } return "--{$this->boundary}\r\n" . trim($str) . "\r\n\r\n"; } protected function createStream(array $elements) { $stream = new AppendStream(); foreach ($elements as $element) { $this->addElement($stream, $element); } $stream->addStream(stream_for("--{$this->boundary}--\r\n")); return $stream; } private function addElement(AppendStream $stream, array $element) { foreach (['contents', 'name'] as $key) { if (!array_key_exists($key, $element)) { throw new \InvalidArgumentException("A '{$key}' key is required"); } } $element['contents'] = stream_for($element['contents']); if (empty($element['filename'])) { $uri = $element['contents']->getMetadata('uri'); if (substr($uri, 0, 6) !== 'php://') { $element['filename'] = $uri; } } list($body, $headers) = $this->createElement( $element['name'], $element['contents'], isset($element['filename']) ? $element['filename'] : null, isset($element['headers']) ? $element['headers'] : [] ); $stream->addStream(stream_for($this->getHeaders($headers))); $stream->addStream($body); $stream->addStream(stream_for("\r\n")); } private function createElement($name, StreamInterface $stream, $filename, array $headers) { $disposition = $this->getHeader($headers, 'content-disposition'); if (!$disposition) { $headers['Content-Disposition'] = ($filename === '0' || $filename) ? sprintf('form-data; name="%s"; filename="%s"', $name, basename($filename)) : "form-data; name=\"{$name}\""; } $length = $this->getHeader($headers, 'content-length'); if (!$length) { if ($length = $stream->getSize()) { $headers['Content-Length'] = (string) $length; } } $type = $this->getHeader($headers, 'content-type'); if (!$type && ($filename === '0' || $filename)) { if ($type = mimetype_from_filename($filename)) { $headers['Content-Type'] = $type; } } return [$stream, $headers]; } private function getHeader(array $headers, $key) { $lowercaseHeader = strtolower($key); foreach ($headers as $k => $v) { if (strtolower($k) === $lowercaseHeader) { return $v; } } return null; } } source = $source; $this->size = isset($options['size']) ? $options['size'] : null; $this->metadata = isset($options['metadata']) ? $options['metadata'] : []; $this->buffer = new BufferStream(); } public function __toString() { try { return copy_to_string($this); } catch (\Exception $e) { return ''; } } public function close() { $this->detach(); } public function detach() { $this->tellPos = false; $this->source = null; } public function getSize() { return $this->size; } public function tell() { return $this->tellPos; } public function eof() { return !$this->source; } public function isSeekable() { return false; } public function rewind() { $this->seek(0); } public function seek($offset, $whence = SEEK_SET) { throw new \RuntimeException('Cannot seek a PumpStream'); } public function isWritable() { return false; } public function write($string) { throw new \RuntimeException('Cannot write to a PumpStream'); } public function isReadable() { return true; } public function read($length) { $data = $this->buffer->read($length); $readLen = strlen($data); $this->tellPos += $readLen; $remaining = $length - $readLen; if ($remaining) { $this->pump($remaining); $data .= $this->buffer->read($remaining); $this->tellPos += strlen($data) - $readLen; } return $data; } public function getContents() { $result = ''; while (!$this->eof()) { $result .= $this->read(1000000); } return $result; } public function getMetadata($key = null) { if (!$key) { return $this->metadata; } return isset($this->metadata[$key]) ? $this->metadata[$key] : null; } private function pump($length) { if ($this->source) { do { $data = call_user_func($this->source, $length); if ($data === false || $data === null) { $this->source = null; return; } $this->buffer->write($data); $length -= strlen($data); } while ($length > 0); } } } method = strtoupper($method); $this->uri = $uri; $this->setHeaders($headers); $this->protocol = $version; if (!isset($this->headerNames['host'])) { $this->updateHostFromUri(); } if ($body !== '' && $body !== null) { $this->stream = stream_for($body); } } public function getRequestTarget() { if ($this->requestTarget !== null) { return $this->requestTarget; } $target = $this->uri->getPath(); if ($target == '') { $target = '/'; } if ($this->uri->getQuery() != '') { $target .= '?' . $this->uri->getQuery(); } return $target; } public function withRequestTarget($requestTarget) { if (preg_match('#\s#', $requestTarget)) { throw new InvalidArgumentException( 'Invalid request target provided; cannot contain whitespace' ); } $new = clone $this; $new->requestTarget = $requestTarget; return $new; } public function getMethod() { return $this->method; } public function withMethod($method) { $new = clone $this; $new->method = strtoupper($method); return $new; } public function getUri() { return $this->uri; } public function withUri(UriInterface $uri, $preserveHost = false) { if ($uri === $this->uri) { return $this; } $new = clone $this; $new->uri = $uri; if (!$preserveHost || !isset($this->headerNames['host'])) { $new->updateHostFromUri(); } return $new; } private function updateHostFromUri() { $host = $this->uri->getHost(); if ($host == '') { return; } if (($port = $this->uri->getPort()) !== null) { $host .= ':' . $port; } if (isset($this->headerNames['host'])) { $header = $this->headerNames['host']; } else { $header = 'Host'; $this->headerNames['host'] = 'Host'; } $this->headers = [$header => [$host]] + $this->headers; } } 'Continue', 101 => 'Switching Protocols', 102 => 'Processing', 200 => 'OK', 201 => 'Created', 202 => 'Accepted', 203 => 'Non-Authoritative Information', 204 => 'No Content', 205 => 'Reset Content', 206 => 'Partial Content', 207 => 'Multi-status', 208 => 'Already Reported', 300 => 'Multiple Choices', 301 => 'Moved Permanently', 302 => 'Found', 303 => 'See Other', 304 => 'Not Modified', 305 => 'Use Proxy', 306 => 'Switch Proxy', 307 => 'Temporary Redirect', 400 => 'Bad Request', 401 => 'Unauthorized', 402 => 'Payment Required', 403 => 'Forbidden', 404 => 'Not Found', 405 => 'Method Not Allowed', 406 => 'Not Acceptable', 407 => 'Proxy Authentication Required', 408 => 'Request Time-out', 409 => 'Conflict', 410 => 'Gone', 411 => 'Length Required', 412 => 'Precondition Failed', 413 => 'Request Entity Too Large', 414 => 'Request-URI Too Large', 415 => 'Unsupported Media Type', 416 => 'Requested range not satisfiable', 417 => 'Expectation Failed', 418 => 'I\'m a teapot', 422 => 'Unprocessable Entity', 423 => 'Locked', 424 => 'Failed Dependency', 425 => 'Unordered Collection', 426 => 'Upgrade Required', 428 => 'Precondition Required', 429 => 'Too Many Requests', 431 => 'Request Header Fields Too Large', 451 => 'Unavailable For Legal Reasons', 500 => 'Internal Server Error', 501 => 'Not Implemented', 502 => 'Bad Gateway', 503 => 'Service Unavailable', 504 => 'Gateway Time-out', 505 => 'HTTP Version not supported', 506 => 'Variant Also Negotiates', 507 => 'Insufficient Storage', 508 => 'Loop Detected', 511 => 'Network Authentication Required', ]; private $reasonPhrase = ''; private $statusCode = 200; public function __construct( $status = 200, array $headers = [], $body = null, $version = '1.1', $reason = null ) { if (filter_var($status, FILTER_VALIDATE_INT) === false) { throw new \InvalidArgumentException('Status code must be an integer value.'); } $this->statusCode = (int) $status; if ($body !== '' && $body !== null) { $this->stream = stream_for($body); } $this->setHeaders($headers); if ($reason == '' && isset(self::$phrases[$this->statusCode])) { $this->reasonPhrase = self::$phrases[$this->statusCode]; } else { $this->reasonPhrase = (string) $reason; } $this->protocol = $version; } public function getStatusCode() { return $this->statusCode; } public function getReasonPhrase() { return $this->reasonPhrase; } public function withStatus($code, $reasonPhrase = '') { $new = clone $this; $new->statusCode = (int) $code; if ($reasonPhrase == '' && isset(self::$phrases[$new->statusCode])) { $reasonPhrase = self::$phrases[$new->statusCode]; } $new->reasonPhrase = $reasonPhrase; return $new; } } @,;:\\\"/[\]?={}\x01-\x20\x7F]++):[ \t]*+((?:[ \t]*+[\x21-\x7E\x80-\xFF]++)*+)[ \t]*+\r?\n)m"; const HEADER_FOLD_REGEX = "(\r?\n[ \t]++)"; } serverParams = $serverParams; parent::__construct($method, $uri, $headers, $body, $version); } public static function normalizeFiles(array $files) { $normalized = []; foreach ($files as $key => $value) { if ($value instanceof UploadedFileInterface) { $normalized[$key] = $value; } elseif (is_array($value) && isset($value['tmp_name'])) { $normalized[$key] = self::createUploadedFileFromSpec($value); } elseif (is_array($value)) { $normalized[$key] = self::normalizeFiles($value); continue; } else { throw new InvalidArgumentException('Invalid value in files specification'); } } return $normalized; } private static function createUploadedFileFromSpec(array $value) { if (is_array($value['tmp_name'])) { return self::normalizeNestedFileSpec($value); } return new UploadedFile( $value['tmp_name'], (int) $value['size'], (int) $value['error'], $value['name'], $value['type'] ); } private static function normalizeNestedFileSpec(array $files = []) { $normalizedFiles = []; foreach (array_keys($files['tmp_name']) as $key) { $spec = [ 'tmp_name' => $files['tmp_name'][$key], 'size' => $files['size'][$key], 'error' => $files['error'][$key], 'name' => $files['name'][$key], 'type' => $files['type'][$key], ]; $normalizedFiles[$key] = self::createUploadedFileFromSpec($spec); } return $normalizedFiles; } public static function fromGlobals() { $method = isset($_SERVER['REQUEST_METHOD']) ? $_SERVER['REQUEST_METHOD'] : 'GET'; $headers = getallheaders(); $uri = self::getUriFromGlobals(); $body = new LazyOpenStream('php://input', 'r+'); $protocol = isset($_SERVER['SERVER_PROTOCOL']) ? str_replace('HTTP/', '', $_SERVER['SERVER_PROTOCOL']) : '1.1'; $serverRequest = new ServerRequest($method, $uri, $headers, $body, $protocol, $_SERVER); return $serverRequest ->withCookieParams($_COOKIE) ->withQueryParams($_GET) ->withParsedBody($_POST) ->withUploadedFiles(self::normalizeFiles($_FILES)); } private static function extractHostAndPortFromAuthority($authority) { $uri = 'http://'.$authority; $parts = parse_url($uri); if (false === $parts) { return [null, null]; } $host = isset($parts['host']) ? $parts['host'] : null; $port = isset($parts['port']) ? $parts['port'] : null; return [$host, $port]; } public static function getUriFromGlobals() { $uri = new Uri(''); $uri = $uri->withScheme(!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off' ? 'https' : 'http'); $hasPort = false; if (isset($_SERVER['HTTP_HOST'])) { list($host, $port) = self::extractHostAndPortFromAuthority($_SERVER['HTTP_HOST']); if ($host !== null) { $uri = $uri->withHost($host); } if ($port !== null) { $hasPort = true; $uri = $uri->withPort($port); } } elseif (isset($_SERVER['SERVER_NAME'])) { $uri = $uri->withHost($_SERVER['SERVER_NAME']); } elseif (isset($_SERVER['SERVER_ADDR'])) { $uri = $uri->withHost($_SERVER['SERVER_ADDR']); } if (!$hasPort && isset($_SERVER['SERVER_PORT'])) { $uri = $uri->withPort($_SERVER['SERVER_PORT']); } $hasQuery = false; if (isset($_SERVER['REQUEST_URI'])) { $requestUriParts = explode('?', $_SERVER['REQUEST_URI'], 2); $uri = $uri->withPath($requestUriParts[0]); if (isset($requestUriParts[1])) { $hasQuery = true; $uri = $uri->withQuery($requestUriParts[1]); } } if (!$hasQuery && isset($_SERVER['QUERY_STRING'])) { $uri = $uri->withQuery($_SERVER['QUERY_STRING']); } return $uri; } public function getServerParams() { return $this->serverParams; } public function getUploadedFiles() { return $this->uploadedFiles; } public function withUploadedFiles(array $uploadedFiles) { $new = clone $this; $new->uploadedFiles = $uploadedFiles; return $new; } public function getCookieParams() { return $this->cookieParams; } public function withCookieParams(array $cookies) { $new = clone $this; $new->cookieParams = $cookies; return $new; } public function getQueryParams() { return $this->queryParams; } public function withQueryParams(array $query) { $new = clone $this; $new->queryParams = $query; return $new; } public function getParsedBody() { return $this->parsedBody; } public function withParsedBody($data) { $new = clone $this; $new->parsedBody = $data; return $new; } public function getAttributes() { return $this->attributes; } public function getAttribute($attribute, $default = null) { if (false === array_key_exists($attribute, $this->attributes)) { return $default; } return $this->attributes[$attribute]; } public function withAttribute($attribute, $value) { $new = clone $this; $new->attributes[$attribute] = $value; return $new; } public function withoutAttribute($attribute) { if (false === array_key_exists($attribute, $this->attributes)) { return $this; } $new = clone $this; unset($new->attributes[$attribute]); return $new; } } [ 'r' => true, 'w+' => true, 'r+' => true, 'x+' => true, 'c+' => true, 'rb' => true, 'w+b' => true, 'r+b' => true, 'x+b' => true, 'c+b' => true, 'rt' => true, 'w+t' => true, 'r+t' => true, 'x+t' => true, 'c+t' => true, 'a+' => true, 'rb+' => true, ], 'write' => [ 'w' => true, 'w+' => true, 'rw' => true, 'r+' => true, 'x+' => true, 'c+' => true, 'wb' => true, 'w+b' => true, 'r+b' => true, 'rb+' => true, 'x+b' => true, 'c+b' => true, 'w+t' => true, 'r+t' => true, 'x+t' => true, 'c+t' => true, 'a' => true, 'a+' => true ] ]; public function __construct($stream, $options = []) { if (!is_resource($stream)) { throw new \InvalidArgumentException('Stream must be a resource'); } if (isset($options['size'])) { $this->size = $options['size']; } $this->customMetadata = isset($options['metadata']) ? $options['metadata'] : []; $this->stream = $stream; $meta = stream_get_meta_data($this->stream); $this->seekable = $meta['seekable']; $this->readable = isset(self::$readWriteHash['read'][$meta['mode']]); $this->writable = isset(self::$readWriteHash['write'][$meta['mode']]); $this->uri = $this->getMetadata('uri'); } public function __destruct() { $this->close(); } public function __toString() { try { $this->seek(0); return (string) stream_get_contents($this->stream); } catch (\Exception $e) { return ''; } } public function getContents() { if (!isset($this->stream)) { throw new \RuntimeException('Stream is detached'); } $contents = stream_get_contents($this->stream); if ($contents === false) { throw new \RuntimeException('Unable to read stream contents'); } return $contents; } public function close() { if (isset($this->stream)) { if (is_resource($this->stream)) { fclose($this->stream); } $this->detach(); } } public function detach() { if (!isset($this->stream)) { return null; } $result = $this->stream; unset($this->stream); $this->size = $this->uri = null; $this->readable = $this->writable = $this->seekable = false; return $result; } public function getSize() { if ($this->size !== null) { return $this->size; } if (!isset($this->stream)) { return null; } if ($this->uri) { clearstatcache(true, $this->uri); } $stats = fstat($this->stream); if (isset($stats['size'])) { $this->size = $stats['size']; return $this->size; } return null; } public function isReadable() { return $this->readable; } public function isWritable() { return $this->writable; } public function isSeekable() { return $this->seekable; } public function eof() { if (!isset($this->stream)) { throw new \RuntimeException('Stream is detached'); } return feof($this->stream); } public function tell() { if (!isset($this->stream)) { throw new \RuntimeException('Stream is detached'); } $result = ftell($this->stream); if ($result === false) { throw new \RuntimeException('Unable to determine stream position'); } return $result; } public function rewind() { $this->seek(0); } public function seek($offset, $whence = SEEK_SET) { if (!isset($this->stream)) { throw new \RuntimeException('Stream is detached'); } if (!$this->seekable) { throw new \RuntimeException('Stream is not seekable'); } if (fseek($this->stream, $offset, $whence) === -1) { throw new \RuntimeException('Unable to seek to stream position ' . $offset . ' with whence ' . var_export($whence, true)); } } public function read($length) { if (!isset($this->stream)) { throw new \RuntimeException('Stream is detached'); } if (!$this->readable) { throw new \RuntimeException('Cannot read from non-readable stream'); } if ($length < 0) { throw new \RuntimeException('Length parameter cannot be negative'); } if (0 === $length) { return ''; } $string = fread($this->stream, $length); if (false === $string) { throw new \RuntimeException('Unable to read from stream'); } return $string; } public function write($string) { if (!isset($this->stream)) { throw new \RuntimeException('Stream is detached'); } if (!$this->writable) { throw new \RuntimeException('Cannot write to a non-writable stream'); } $this->size = null; $result = fwrite($this->stream, $string); if ($result === false) { throw new \RuntimeException('Unable to write to stream'); } return $result; } public function getMetadata($key = null) { if (!isset($this->stream)) { return $key ? null : []; } elseif (!$key) { return $this->customMetadata + stream_get_meta_data($this->stream); } elseif (isset($this->customMetadata[$key])) { return $this->customMetadata[$key]; } $meta = stream_get_meta_data($this->stream); return isset($meta[$key]) ? $meta[$key] : null; } } stream = $stream; } public function __get($name) { if ($name == 'stream') { $this->stream = $this->createStream(); return $this->stream; } throw new \UnexpectedValueException("$name not found on class"); } public function __toString() { try { if ($this->isSeekable()) { $this->seek(0); } return $this->getContents(); } catch (\Exception $e) { trigger_error('StreamDecorator::__toString exception: ' . (string) $e, E_USER_ERROR); return ''; } } public function getContents() { return copy_to_string($this); } public function __call($method, array $args) { $result = call_user_func_array([$this->stream, $method], $args); return $result === $this->stream ? $this : $result; } public function close() { $this->stream->close(); } public function getMetadata($key = null) { return $this->stream->getMetadata($key); } public function detach() { return $this->stream->detach(); } public function getSize() { return $this->stream->getSize(); } public function eof() { return $this->stream->eof(); } public function tell() { return $this->stream->tell(); } public function isReadable() { return $this->stream->isReadable(); } public function isWritable() { return $this->stream->isWritable(); } public function isSeekable() { return $this->stream->isSeekable(); } public function rewind() { $this->seek(0); } public function seek($offset, $whence = SEEK_SET) { $this->stream->seek($offset, $whence); } public function read($length) { return $this->stream->read($length); } public function write($string) { return $this->stream->write($string); } protected function createStream() { throw new \BadMethodCallException('Not implemented'); } } isReadable()) { $mode = $stream->isWritable() ? 'r+' : 'r'; } elseif ($stream->isWritable()) { $mode = 'w'; } else { throw new \InvalidArgumentException('The stream must be readable, ' . 'writable, or both.'); } return fopen('guzzle://stream', $mode, null, self::createStreamContext($stream)); } public static function createStreamContext(StreamInterface $stream) { return stream_context_create([ 'guzzle' => ['stream' => $stream] ]); } public static function register() { if (!in_array('guzzle', stream_get_wrappers())) { stream_wrapper_register('guzzle', __CLASS__); } } public function stream_open($path, $mode, $options, &$opened_path) { $options = stream_context_get_options($this->context); if (!isset($options['guzzle']['stream'])) { return false; } $this->mode = $mode; $this->stream = $options['guzzle']['stream']; return true; } public function stream_read($count) { return $this->stream->read($count); } public function stream_write($data) { return (int) $this->stream->write($data); } public function stream_tell() { return $this->stream->tell(); } public function stream_eof() { return $this->stream->eof(); } public function stream_seek($offset, $whence) { $this->stream->seek($offset, $whence); return true; } public function stream_cast($cast_as) { $stream = clone($this->stream); return $stream->detach(); } public function stream_stat() { static $modeMap = [ 'r' => 33060, 'rb' => 33060, 'r+' => 33206, 'w' => 33188, 'wb' => 33188 ]; return [ 'dev' => 0, 'ino' => 0, 'mode' => $modeMap[$this->mode], 'nlink' => 0, 'uid' => 0, 'gid' => 0, 'rdev' => 0, 'size' => $this->stream->getSize() ?: 0, 'atime' => 0, 'mtime' => 0, 'ctime' => 0, 'blksize' => 0, 'blocks' => 0 ]; } public function url_stat($path, $flags) { return [ 'dev' => 0, 'ino' => 0, 'mode' => 0, 'nlink' => 0, 'uid' => 0, 'gid' => 0, 'rdev' => 0, 'size' => 0, 'atime' => 0, 'mtime' => 0, 'ctime' => 0, 'blksize' => 0, 'blocks' => 0 ]; } } setError($errorStatus); $this->setSize($size); $this->setClientFilename($clientFilename); $this->setClientMediaType($clientMediaType); if ($this->isOk()) { $this->setStreamOrFile($streamOrFile); } } private function setStreamOrFile($streamOrFile) { if (is_string($streamOrFile)) { $this->file = $streamOrFile; } elseif (is_resource($streamOrFile)) { $this->stream = new Stream($streamOrFile); } elseif ($streamOrFile instanceof StreamInterface) { $this->stream = $streamOrFile; } else { throw new InvalidArgumentException( 'Invalid stream or file provided for UploadedFile' ); } } private function setError($error) { if (false === is_int($error)) { throw new InvalidArgumentException( 'Upload file error status must be an integer' ); } if (false === in_array($error, UploadedFile::$errors)) { throw new InvalidArgumentException( 'Invalid error status for UploadedFile' ); } $this->error = $error; } private function setSize($size) { if (false === is_int($size)) { throw new InvalidArgumentException( 'Upload file size must be an integer' ); } $this->size = $size; } private function isStringOrNull($param) { return in_array(gettype($param), ['string', 'NULL']); } private function isStringNotEmpty($param) { return is_string($param) && false === empty($param); } private function setClientFilename($clientFilename) { if (false === $this->isStringOrNull($clientFilename)) { throw new InvalidArgumentException( 'Upload file client filename must be a string or null' ); } $this->clientFilename = $clientFilename; } private function setClientMediaType($clientMediaType) { if (false === $this->isStringOrNull($clientMediaType)) { throw new InvalidArgumentException( 'Upload file client media type must be a string or null' ); } $this->clientMediaType = $clientMediaType; } private function isOk() { return $this->error === UPLOAD_ERR_OK; } public function isMoved() { return $this->moved; } private function validateActive() { if (false === $this->isOk()) { throw new RuntimeException('Cannot retrieve stream due to upload error'); } if ($this->isMoved()) { throw new RuntimeException('Cannot retrieve stream after it has already been moved'); } } public function getStream() { $this->validateActive(); if ($this->stream instanceof StreamInterface) { return $this->stream; } return new LazyOpenStream($this->file, 'r+'); } public function moveTo($targetPath) { $this->validateActive(); if (false === $this->isStringNotEmpty($targetPath)) { throw new InvalidArgumentException( 'Invalid path provided for move operation; must be a non-empty string' ); } if ($this->file) { $this->moved = php_sapi_name() == 'cli' ? rename($this->file, $targetPath) : move_uploaded_file($this->file, $targetPath); } else { copy_to_stream( $this->getStream(), new LazyOpenStream($targetPath, 'w') ); $this->moved = true; } if (false === $this->moved) { throw new RuntimeException( sprintf('Uploaded file could not be moved to %s', $targetPath) ); } } public function getSize() { return $this->size; } public function getError() { return $this->error; } public function getClientFilename() { return $this->clientFilename; } public function getClientMediaType() { return $this->clientMediaType; } } 80, 'https' => 443, 'ftp' => 21, 'gopher' => 70, 'nntp' => 119, 'news' => 119, 'telnet' => 23, 'tn3270' => 23, 'imap' => 143, 'pop' => 110, 'ldap' => 389, ]; private static $charUnreserved = 'a-zA-Z0-9_\-\.~'; private static $charSubDelims = '!\$&\'\(\)\*\+,;='; private static $replaceQuery = ['=' => '%3D', '&' => '%26']; private $scheme = ''; private $userInfo = ''; private $host = ''; private $port; private $path = ''; private $query = ''; private $fragment = ''; public function __construct($uri = '') { if ($uri != '') { $parts = parse_url($uri); if ($parts === false) { throw new \InvalidArgumentException("Unable to parse URI: $uri"); } $this->applyParts($parts); } } public function __toString() { return self::composeComponents( $this->scheme, $this->getAuthority(), $this->path, $this->query, $this->fragment ); } public static function composeComponents($scheme, $authority, $path, $query, $fragment) { $uri = ''; if ($scheme != '') { $uri .= $scheme . ':'; } if ($authority != ''|| $scheme === 'file') { $uri .= '//' . $authority; } $uri .= $path; if ($query != '') { $uri .= '?' . $query; } if ($fragment != '') { $uri .= '#' . $fragment; } return $uri; } public static function isDefaultPort(UriInterface $uri) { return $uri->getPort() === null || (isset(self::$defaultPorts[$uri->getScheme()]) && $uri->getPort() === self::$defaultPorts[$uri->getScheme()]); } public static function isAbsolute(UriInterface $uri) { return $uri->getScheme() !== ''; } public static function isNetworkPathReference(UriInterface $uri) { return $uri->getScheme() === '' && $uri->getAuthority() !== ''; } public static function isAbsolutePathReference(UriInterface $uri) { return $uri->getScheme() === '' && $uri->getAuthority() === '' && isset($uri->getPath()[0]) && $uri->getPath()[0] === '/'; } public static function isRelativePathReference(UriInterface $uri) { return $uri->getScheme() === '' && $uri->getAuthority() === '' && (!isset($uri->getPath()[0]) || $uri->getPath()[0] !== '/'); } public static function isSameDocumentReference(UriInterface $uri, UriInterface $base = null) { if ($base !== null) { $uri = UriResolver::resolve($base, $uri); return ($uri->getScheme() === $base->getScheme()) && ($uri->getAuthority() === $base->getAuthority()) && ($uri->getPath() === $base->getPath()) && ($uri->getQuery() === $base->getQuery()); } return $uri->getScheme() === '' && $uri->getAuthority() === '' && $uri->getPath() === '' && $uri->getQuery() === ''; } public static function removeDotSegments($path) { return UriResolver::removeDotSegments($path); } public static function resolve(UriInterface $base, $rel) { if (!($rel instanceof UriInterface)) { $rel = new self($rel); } return UriResolver::resolve($base, $rel); } public static function withoutQueryValue(UriInterface $uri, $key) { $result = self::getFilteredQueryString($uri, [$key]); return $uri->withQuery(implode('&', $result)); } public static function withQueryValue(UriInterface $uri, $key, $value) { $result = self::getFilteredQueryString($uri, [$key]); $result[] = self::generateQueryString($key, $value); return $uri->withQuery(implode('&', $result)); } public static function withQueryValues(UriInterface $uri, array $keyValueArray) { $result = self::getFilteredQueryString($uri, array_keys($keyValueArray)); foreach ($keyValueArray as $key => $value) { $result[] = self::generateQueryString($key, $value); } return $uri->withQuery(implode('&', $result)); } public static function fromParts(array $parts) { $uri = new self(); $uri->applyParts($parts); $uri->validateState(); return $uri; } public function getScheme() { return $this->scheme; } public function getAuthority() { $authority = $this->host; if ($this->userInfo !== '') { $authority = $this->userInfo . '@' . $authority; } if ($this->port !== null) { $authority .= ':' . $this->port; } return $authority; } public function getUserInfo() { return $this->userInfo; } public function getHost() { return $this->host; } public function getPort() { return $this->port; } public function getPath() { return $this->path; } public function getQuery() { return $this->query; } public function getFragment() { return $this->fragment; } public function withScheme($scheme) { $scheme = $this->filterScheme($scheme); if ($this->scheme === $scheme) { return $this; } $new = clone $this; $new->scheme = $scheme; $new->removeDefaultPort(); $new->validateState(); return $new; } public function withUserInfo($user, $password = null) { $info = $user; if ($password != '') { $info .= ':' . $password; } if ($this->userInfo === $info) { return $this; } $new = clone $this; $new->userInfo = $info; $new->validateState(); return $new; } public function withHost($host) { $host = $this->filterHost($host); if ($this->host === $host) { return $this; } $new = clone $this; $new->host = $host; $new->validateState(); return $new; } public function withPort($port) { $port = $this->filterPort($port); if ($this->port === $port) { return $this; } $new = clone $this; $new->port = $port; $new->removeDefaultPort(); $new->validateState(); return $new; } public function withPath($path) { $path = $this->filterPath($path); if ($this->path === $path) { return $this; } $new = clone $this; $new->path = $path; $new->validateState(); return $new; } public function withQuery($query) { $query = $this->filterQueryAndFragment($query); if ($this->query === $query) { return $this; } $new = clone $this; $new->query = $query; return $new; } public function withFragment($fragment) { $fragment = $this->filterQueryAndFragment($fragment); if ($this->fragment === $fragment) { return $this; } $new = clone $this; $new->fragment = $fragment; return $new; } private function applyParts(array $parts) { $this->scheme = isset($parts['scheme']) ? $this->filterScheme($parts['scheme']) : ''; $this->userInfo = isset($parts['user']) ? $parts['user'] : ''; $this->host = isset($parts['host']) ? $this->filterHost($parts['host']) : ''; $this->port = isset($parts['port']) ? $this->filterPort($parts['port']) : null; $this->path = isset($parts['path']) ? $this->filterPath($parts['path']) : ''; $this->query = isset($parts['query']) ? $this->filterQueryAndFragment($parts['query']) : ''; $this->fragment = isset($parts['fragment']) ? $this->filterQueryAndFragment($parts['fragment']) : ''; if (isset($parts['pass'])) { $this->userInfo .= ':' . $parts['pass']; } $this->removeDefaultPort(); } private function filterScheme($scheme) { if (!is_string($scheme)) { throw new \InvalidArgumentException('Scheme must be a string'); } return strtolower($scheme); } private function filterHost($host) { if (!is_string($host)) { throw new \InvalidArgumentException('Host must be a string'); } return strtolower($host); } private function filterPort($port) { if ($port === null) { return null; } $port = (int) $port; if (1 > $port || 0xffff < $port) { throw new \InvalidArgumentException( sprintf('Invalid port: %d. Must be between 1 and 65535', $port) ); } return $port; } private static function getFilteredQueryString(UriInterface $uri, array $keys) { $current = $uri->getQuery(); if ($current === '') { return []; } $decodedKeys = array_map('rawurldecode', $keys); return array_filter(explode('&', $current), function ($part) use ($decodedKeys) { return !in_array(rawurldecode(explode('=', $part)[0]), $decodedKeys, true); }); } private static function generateQueryString($key, $value) { $queryString = strtr($key, self::$replaceQuery); if ($value !== null) { $queryString .= '=' . strtr($value, self::$replaceQuery); } return $queryString; } private function removeDefaultPort() { if ($this->port !== null && self::isDefaultPort($this)) { $this->port = null; } } private function filterPath($path) { if (!is_string($path)) { throw new \InvalidArgumentException('Path must be a string'); } return preg_replace_callback( '/(?:[^' . self::$charUnreserved . self::$charSubDelims . '%:@\/]++|%(?![A-Fa-f0-9]{2}))/', [$this, 'rawurlencodeMatchZero'], $path ); } private function filterQueryAndFragment($str) { if (!is_string($str)) { throw new \InvalidArgumentException('Query and fragment must be a string'); } return preg_replace_callback( '/(?:[^' . self::$charUnreserved . self::$charSubDelims . '%:@\/\?]++|%(?![A-Fa-f0-9]{2}))/', [$this, 'rawurlencodeMatchZero'], $str ); } private function rawurlencodeMatchZero(array $match) { return rawurlencode($match[0]); } private function validateState() { if ($this->host === '' && ($this->scheme === 'http' || $this->scheme === 'https')) { $this->host = self::HTTP_DEFAULT_HOST; } if ($this->getAuthority() === '') { if (0 === strpos($this->path, '//')) { throw new \InvalidArgumentException('The path of a URI without an authority must not start with two slashes "//"'); } if ($this->scheme === '' && false !== strpos(explode('/', $this->path, 2)[0], ':')) { throw new \InvalidArgumentException('A relative URI must not have a path beginning with a segment containing a colon'); } } elseif (isset($this->path[0]) && $this->path[0] !== '/') { @trigger_error( 'The path of a URI with an authority must start with a slash "/" or be empty. Automagically fixing the URI ' . 'by adding a leading slash to the path is deprecated since version 1.4 and will throw an exception instead.', E_USER_DEPRECATED ); $this->path = '/'. $this->path; } } } getPath() === '' && ($uri->getScheme() === 'http' || $uri->getScheme() === 'https') ) { $uri = $uri->withPath('/'); } if ($flags & self::REMOVE_DEFAULT_HOST && $uri->getScheme() === 'file' && $uri->getHost() === 'localhost') { $uri = $uri->withHost(''); } if ($flags & self::REMOVE_DEFAULT_PORT && $uri->getPort() !== null && Uri::isDefaultPort($uri)) { $uri = $uri->withPort(null); } if ($flags & self::REMOVE_DOT_SEGMENTS && !Uri::isRelativePathReference($uri)) { $uri = $uri->withPath(UriResolver::removeDotSegments($uri->getPath())); } if ($flags & self::REMOVE_DUPLICATE_SLASHES) { $uri = $uri->withPath(preg_replace('#//++#', '/', $uri->getPath())); } if ($flags & self::SORT_QUERY_PARAMETERS && $uri->getQuery() !== '') { $queryKeyValues = explode('&', $uri->getQuery()); sort($queryKeyValues); $uri = $uri->withQuery(implode('&', $queryKeyValues)); } return $uri; } public static function isEquivalent(UriInterface $uri1, UriInterface $uri2, $normalizations = self::PRESERVING_NORMALIZATIONS) { return (string) self::normalize($uri1, $normalizations) === (string) self::normalize($uri2, $normalizations); } private static function capitalizePercentEncoding(UriInterface $uri) { $regex = '/(?:%[A-Fa-f0-9]{2})++/'; $callback = function (array $match) { return strtoupper($match[0]); }; return $uri->withPath( preg_replace_callback($regex, $callback, $uri->getPath()) )->withQuery( preg_replace_callback($regex, $callback, $uri->getQuery()) ); } private static function decodeUnreservedCharacters(UriInterface $uri) { $regex = '/%(?:2D|2E|5F|7E|3[0-9]|[46][1-9A-F]|[57][0-9A])/i'; $callback = function (array $match) { return rawurldecode($match[0]); }; return $uri->withPath( preg_replace_callback($regex, $callback, $uri->getPath()) )->withQuery( preg_replace_callback($regex, $callback, $uri->getQuery()) ); } private function __construct() { } } getScheme() != '') { return $rel->withPath(self::removeDotSegments($rel->getPath())); } if ($rel->getAuthority() != '') { $targetAuthority = $rel->getAuthority(); $targetPath = self::removeDotSegments($rel->getPath()); $targetQuery = $rel->getQuery(); } else { $targetAuthority = $base->getAuthority(); if ($rel->getPath() === '') { $targetPath = $base->getPath(); $targetQuery = $rel->getQuery() != '' ? $rel->getQuery() : $base->getQuery(); } else { if ($rel->getPath()[0] === '/') { $targetPath = $rel->getPath(); } else { if ($targetAuthority != '' && $base->getPath() === '') { $targetPath = '/' . $rel->getPath(); } else { $lastSlashPos = strrpos($base->getPath(), '/'); if ($lastSlashPos === false) { $targetPath = $rel->getPath(); } else { $targetPath = substr($base->getPath(), 0, $lastSlashPos + 1) . $rel->getPath(); } } } $targetPath = self::removeDotSegments($targetPath); $targetQuery = $rel->getQuery(); } } return new Uri(Uri::composeComponents( $base->getScheme(), $targetAuthority, $targetPath, $targetQuery, $rel->getFragment() )); } public static function relativize(UriInterface $base, UriInterface $target) { if ($target->getScheme() !== '' && ($base->getScheme() !== $target->getScheme() || $target->getAuthority() === '' && $base->getAuthority() !== '') ) { return $target; } if (Uri::isRelativePathReference($target)) { return $target; } if ($target->getAuthority() !== '' && $base->getAuthority() !== $target->getAuthority()) { return $target->withScheme(''); } $emptyPathUri = $target->withScheme('')->withPath('')->withUserInfo('')->withPort(null)->withHost(''); if ($base->getPath() !== $target->getPath()) { return $emptyPathUri->withPath(self::getRelativePath($base, $target)); } if ($base->getQuery() === $target->getQuery()) { return $emptyPathUri->withQuery(''); } if ($target->getQuery() === '') { $segments = explode('/', $target->getPath()); $lastSegment = end($segments); return $emptyPathUri->withPath($lastSegment === '' ? './' : $lastSegment); } return $emptyPathUri; } private static function getRelativePath(UriInterface $base, UriInterface $target) { $sourceSegments = explode('/', $base->getPath()); $targetSegments = explode('/', $target->getPath()); array_pop($sourceSegments); $targetLastSegment = array_pop($targetSegments); foreach ($sourceSegments as $i => $segment) { if (isset($targetSegments[$i]) && $segment === $targetSegments[$i]) { unset($sourceSegments[$i], $targetSegments[$i]); } else { break; } } $targetSegments[] = $targetLastSegment; $relativePath = str_repeat('../', count($sourceSegments)) . implode('/', $targetSegments); if ('' === $relativePath || false !== strpos(explode('/', $relativePath, 2)[0], ':')) { $relativePath = "./$relativePath"; } elseif ('/' === $relativePath[0]) { if ($base->getAuthority() != '' && $base->getPath() === '') { $relativePath = ".$relativePath"; } else { $relativePath = "./$relativePath"; } } return $relativePath; } private function __construct() { } } _namespacePrefixesToBaseDirectories[$prefix])) { $this->_namespacePrefixesToBaseDirectories[$prefix] = []; } if (true === $prepend) { array_unshift( $this->_namespacePrefixesToBaseDirectories[$prefix], $baseDirectory ); } else { array_push( $this->_namespacePrefixesToBaseDirectories[$prefix], $baseDirectory ); } return; } public function load($entity) { $entityPrefix = $entity; $hasBaseDirectory = false; while (false !== $pos = strrpos($entityPrefix, '\\')) { $currentEntityPrefix = substr($entity, 0, $pos + 1); $entityPrefix = rtrim($currentEntityPrefix, '\\'); $entitySuffix = substr($entity, $pos + 1); $entitySuffixAsPath = str_replace('\\', '/', $entitySuffix); if (false === $this->hasBaseDirectory($currentEntityPrefix)) { continue; } $hasBaseDirectory = true; foreach ($this->getBaseDirectories($currentEntityPrefix) as $baseDirectory) { $file = $baseDirectory . $entitySuffixAsPath . '.php'; if (false !== $this->requireFile($file)) { return $file; } } } if (true === $hasBaseDirectory && $entity === Consistency::getEntityShortestName($entity) && false !== $pos = strrpos($entity, '\\')) { return $this->runAutoloaderStack( $entity . '\\' . substr($entity, $pos + 1) ); } return null; } public function requireFile($filename) { if (false === file_exists($filename)) { return false; } require $filename; return true; } public function hasBaseDirectory($namespacePrefix) { return isset($this->_namespacePrefixesToBaseDirectories[$namespacePrefix]); } public function getBaseDirectories($namespacePrefix) { if (false === $this->hasBaseDirectory($namespacePrefix)) { return []; } return $this->_namespacePrefixesToBaseDirectories[$namespacePrefix]; } public static function getLoadedClasses() { return get_declared_classes(); } public function runAutoloaderStack($entity) { return spl_autoload_call($entity); } public function register($prepend = false) { return spl_autoload_register([$this, 'load'], true, $prepend); } public function unregister() { return spl_autoload_unregister([$this, 'load']); } public function getRegisteredAutoloaders() { return spl_autoload_functions(); } public static function dnew($classname, array $arguments = []) { $classname = ltrim($classname, '\\'); if (false === Consistency::entityExists($classname, false)) { spl_autoload_call($classname); } $class = new \ReflectionClass($classname); if (empty($arguments) || false === $class->hasMethod('__construct')) { return $class->newInstance(); } return $class->newInstanceArgs($arguments); } } $autoloader = new Autoloader(); $autoloader->addNamespace('Hoa', dirname(__DIR__)); $autoloader->register(); = $count) { return $entityName; } if ($parts[$count - 2] === $parts[$count - 1]) { return implode('\\', array_slice($parts, 0, -1)); } return $entityName; } public static function flexEntity($entityName) { return class_alias( $entityName, static::getEntityShortestName($entityName), false ); } public static function isKeyword($word) { static $_list = [ '__halt_compiler', 'abstract', 'and', 'array', 'as', 'bool', 'break', 'callable', 'case', 'catch', 'class', 'clone', 'const', 'continue', 'declare', 'default', 'die', 'do', 'echo', 'else', 'elseif', 'empty', 'enddeclare', 'endfor', 'endforeach', 'endif', 'endswitch', 'endwhile', 'eval', 'exit', 'extends', 'false', 'final', 'float', 'for', 'foreach', 'function', 'global', 'goto', 'if', 'implements', 'include', 'include_once', 'instanceof', 'insteadof', 'int', 'interface', 'isset', 'list', 'mixed', 'namespace', 'new', 'null', 'numeric', 'object', 'or', 'print', 'private', 'protected', 'public', 'require', 'require_once', 'resource', 'return', 'static', 'string', 'switch', 'throw', 'trait', 'true', 'try', 'unset', 'use', 'var', 'void', 'while', 'xor', 'yield', '__class__', '__dir__', '__file__', '__function__', '__line__', '__method__', '__namespace__', '__trait__' ]; return in_array(strtolower($word), $_list); } public static function isIdentifier($id) { return 0 !== preg_match( '#^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x80-\xff]*$#', $id ); } public static function registerShutdownFunction($callable) { return register_shutdown_function($callable); } public static function getPHPBinary() { if (defined('PHP_BINARY')) { return PHP_BINARY; } if (isset($_SERVER['_'])) { return $_SERVER['_']; } foreach (['', '.exe'] as $extension) { if (file_exists($_ = PHP_BINDIR . DS . 'php' . $extension)) { return realpath($_); } } return null; } public static function uuid() { return sprintf( '%04x%04x-%04x-%04x-%04x-%04x%04x%04x', mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0x0fff) | 0x4000, mt_rand(0, 0x3fff) | 0x8000, mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff) ); } } } namespace { if (70000 > PHP_VERSION_ID && false === interface_exists('Throwable', false)) { interface Throwable { public function getMessage(); public function getCode(); public function getFile(); public function getLine(); public function getTrace(); public function getPrevious(); public function getTraceAsString(); public function __toString(); } } if (50600 > PHP_VERSION_ID) { $define = function ($constantName, $constantValue, $case = false) { if (!defined($constantName)) { return define($constantName, $constantValue, $case); } return false; }; $define('STREAM_CRYPTO_METHOD_TLSv1_0_SERVER', 8); $define('STREAM_CRYPTO_METHOD_TLSv1_1_SERVER', 16); $define('STREAM_CRYPTO_METHOD_TLSv1_2_SERVER', 32); $define('STREAM_CRYPTO_METHOD_ANY_SERVER', 62); $define('STREAM_CRYPTO_METHOD_TLSv1_0_CLIENT', 9); $define('STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT', 17); $define('STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT', 33); $define('STREAM_CRYPTO_METHOD_ANY_CLIENT', 63); } if (!function_exists('curry')) { function curry($callable) { $arguments = func_get_args(); array_shift($arguments); $ii = array_keys($arguments, …, true); return function () use ($callable, $arguments, $ii) { return call_user_func_array( $callable, array_replace($arguments, array_combine($ii, func_get_args())) ); }; } } Hoa\Consistency\Consistency::flexEntity('Hoa\Consistency\Consistency'); } given( $autoloader = new SUT(), $prefix = 'Foo\Bar\\', $baseDirectoryA = 'Source/Foo/Bar/', $baseDirectoryB = 'Source/Foo/Bar/' ) ->when( $autoloader->addNamespace($prefix, $baseDirectoryA), $result = $autoloader->addNamespace($prefix, $baseDirectoryB) ) ->then ->boolean($autoloader->hasBaseDirectory($prefix)) ->isTrue() ->array($autoloader->getBaseDirectories($prefix)) ->isEqualTo([ $baseDirectoryB, $baseDirectoryA ]); } public function case_add_namespace_append() { $this ->given( $autoloader = new SUT(), $prefix = 'Foo\Bar\\', $baseDirectoryA = 'Source/Foo/Bar/', $baseDirectoryB = 'Source/Foo/Bar/' ) ->when( $autoloader->addNamespace($prefix, $baseDirectoryA), $result = $autoloader->addNamespace($prefix, $baseDirectoryB) ) ->then ->boolean($autoloader->hasBaseDirectory($prefix)) ->isTrue() ->array($autoloader->getBaseDirectories($prefix)) ->isEqualTo([ $baseDirectoryA, $baseDirectoryB ]); } public function case_add_namespace_with_invalid_prefix() { $this ->given( $autoloader = new SUT(), $prefix = '\\\\Foo\Bar', $baseDirectory = 'Source/Foo/Bar/' ) ->when($result = $autoloader->addNamespace($prefix, $baseDirectory)) ->then ->boolean($autoloader->hasBaseDirectory('Foo\Bar\\')) ->isTrue() ->array($autoloader->getBaseDirectories('Foo\Bar\\')) ->isEqualTo([$baseDirectory]); } public function case_add_namespace_with_invalid_base_directory() { $this ->given( $autoloader = new SUT(), $prefix = 'Foo\Bar\\', $baseDirectory = 'Source/Foo/Bar' ) ->when($result = $autoloader->addNamespace($prefix, $baseDirectory)) ->then ->boolean($autoloader->hasBaseDirectory('Foo\Bar\\')) ->isTrue() ->array($autoloader->getBaseDirectories('Foo\Bar\\')) ->isEqualTo(['Source/Foo/Bar/']); } public function case_add_namespace_with_crazy_invalid_base_directory() { $this ->given( $autoloader = new SUT(), $prefix = 'Foo\Bar\\', $baseDirectory = 'Source/Foo/Bar/////' ) ->when($result = $autoloader->addNamespace($prefix, $baseDirectory)) ->then ->boolean($autoloader->hasBaseDirectory('Foo\Bar\\')) ->isTrue() ->array($autoloader->getBaseDirectories('Foo\Bar\\')) ->isEqualTo(['Source/Foo/Bar/']); } public function case_load() { $this ->given( $autoloader = new \Mock\Hoa\Consistency\Autoloader(), $autoloader->addNamespace('Foo\Bar\\', 'Source/Foo/Bar/'), $this->calling($autoloader)->requireFile = function ($file) { return $file; } ) ->when($result = $autoloader->load('Foo\Bar\Baz\Qux')) ->then ->string($result) ->isEqualTo('Source/Foo/Bar/Baz/Qux.php'); } public function case_load_invalid_entity() { $this ->given($autoloader = new SUT()) ->when($result = $autoloader->load('Foo')) ->then ->variable($result) ->isNull(); } public function case_load_flex_entity() { $self = $this; $this ->given( $autoloader = new \Mock\Hoa\Consistency\Autoloader(), $autoloader->addNamespace('Foo\Bar\\', 'Source/Foo/'), $this->calling($autoloader)->runAutoloaderStack = function ($entity) use ($self, &$called) { $called = true; $self ->string($entity) ->isEqualTo('Foo\Bar\Baz\Baz'); return; }, $autoloader->register() ) ->when($result = $autoloader->load('Foo\Bar\Baz')) ->then ->variable($result) ->isNull() ->boolean($called) ->isTrue(); } public function case_load_unmapped_flex_entity() { $self = $this; $this ->given( $autoloader = new \Mock\Hoa\Consistency\Autoloader(), $this->calling($autoloader)->runAutoloaderStack = function ($entity) use ($self, &$called) { $called = true; return; }, $autoloader->register() ) ->when($result = $autoloader->load('Foo\Bar\Baz')) ->then ->variable($result) ->isNull() ->variable($called) ->isNull(); } public function case_require_existing_file() { $this ->given( $autoloader = new SUT(), $this->function->file_exists = true, $constantName = 'HOA_TEST_' . uniqid(), $filename = 'hoa://Test/Vfs/Foo?type=file', file_put_contents($filename, 'when($result = $autoloader->requireFile($filename)) ->then ->boolean($result) ->isTrue() ->string(constant($constantName)) ->isEqualTo('BAR'); } public function case_require_not_existing_file() { $this ->given( $autoloader = new SUT(), $this->function->file_exists = false ) ->when($result = $autoloader->requireFile('/hoa/flatland')) ->then ->boolean($result) ->isFalse(); } public function case_has_not_base_directory() { $this ->given($autoloader = new SUT()) ->when($result = $autoloader->hasBaseDirectory('foo')) ->then ->boolean($result) ->isFalse(); } public function case_get_base_undeclared_namespace_prefix() { $this ->given($autoloader = new SUT()) ->when($result = $autoloader->getBaseDirectories('foo')) ->then ->array($result) ->isEmpty(); } public function case_dnew() { $this ->given($classname = 'Hoa\Consistency\Autoloader') ->when($result = SUT::dnew($classname)) ->then ->object($result) ->isInstanceOf($classname); } public function case_dnew_unknown_class() { $this ->given($this->function->spl_autoload_call = null) ->exception(function () { SUT::dnew('Foo'); }) ->isInstanceOf('ReflectionException'); } public function case_get_loaded_classes() { $this ->given( $declaredClasses = get_declared_classes(), $this->function->get_declared_classes = $declaredClasses ) ->when($result = SUT::getLoadedClasses()) ->then ->array($result) ->isEqualTo($declaredClasses); } public function case_register() { $self = $this; $this ->given($autoloader = new SUT()) ->when($result = $autoloader->register()) ->then ->boolean($result) ->isTrue() ->array($autoloader->getRegisteredAutoloaders()) ->isEqualTo(spl_autoload_functions()); } public function case_unregister() { $this ->given( $autoloader = new SUT(), $oldRegisteredAutoloaders = $autoloader->getRegisteredAutoloaders() ) ->when($result = $autoloader->register()) ->then ->boolean($result) ->isTrue() ->integer(count($autoloader->getRegisteredAutoloaders())) ->isEqualTo(count($oldRegisteredAutoloaders) + 1) ->when($result = $autoloader->unregister()) ->then ->boolean($result) ->isTrue() ->array($autoloader->getRegisteredAutoloaders()) ->isEqualTo($oldRegisteredAutoloaders); } } given( $this->function->class_exists = $class, $this->function->interface_exists = $interface, $this->function->trait_exists = $trait ) ->when($result = SUT::entityExists('foo')) ->then ->boolean($result) ->isTrue(); } public function case_entity_exists_with_class() { return $this->_entity_exists_with_xxx(true, false, false); } public function case_entity_exists_with_interface() { return $this->_entity_exists_with_xxx(false, true, false); } public function case_entity_exists_with_trait() { return $this->_entity_exists_with_xxx(false, false, true); } public function case_entity_does_not_exists() { $this ->given( $this->function->class_exists = false, $this->function->interface_exists = false, $this->function->trait_exists = false ) ->when($result = SUT::entityExists('foo')) ->then ->boolean($result) ->isFalse(); } public function case_get_entity_shortest_name() { $this ->when($result = SUT::getEntityShortestName('Foo\Bar\Bar')) ->then ->string($result) ->isEqualTo('Foo\Bar'); } public function case_get_entity_shortest_name_with_already_the_shortest() { $this ->when($result = SUT::getEntityShortestName('Foo\Bar')) ->then ->string($result) ->isEqualTo('Foo\Bar'); } public function case_get_entity_shortest_name_with_no_namespace() { $this ->when($result = SUT::getEntityShortestName('Foo')) ->then ->string($result) ->isEqualTo('Foo'); } public function case_is_keyword() { $this ->given( $keywords = [ '__HALT_COMPILER', 'abstract', 'and', 'array', 'as', 'bool', 'break', 'callable', 'case', 'catch', 'class', 'clone', 'const', 'continue', 'declare', 'default', 'die', 'do', 'echo', 'else', 'elseif', 'empty', 'enddeclare', 'endfor', 'endforeach', 'endif', 'endswitch', 'endwhile', 'eval', 'exit', 'extends', 'false', 'final', 'float', 'for', 'foreach', 'function', 'global', 'goto', 'if', 'implements', 'include', 'include_once', 'instanceof', 'insteadof', 'int', 'interface', 'isset', 'list', 'mixed', 'namespace', 'new', 'null', 'numeric', 'object', 'or', 'print', 'private', 'protected', 'public', 'require', 'require_once', 'resource', 'return', 'static', 'string', 'switch', 'throw', 'trait', 'true', 'try', 'unset', 'use', 'var', 'void', 'while', 'xor', 'yield', '__CLASS__', '__DIR__', '__FILE__', '__FUNCTION__', '__LINE__', '__METHOD__', '__NAMESPACE__', '__TRAIT__' ] ) ->when(function () use ($keywords) { foreach ($keywords as $keyword) { $this ->boolean(SUT::isKeyword($keyword)) ->isTrue(); } }); } public function case_is_identifier() { $this ->given($_identifier = $this->realdom->regex('#^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x80-\xff]*$#')) ->when(function () use ($_identifier) { foreach ($this->sampleMany($_identifier, 1000) as $identifier) { $this ->boolean(SUT::isIdentifier($identifier)) ->isTrue(); } }); } public function case_register_shutdown_function() { $self = $this; $this ->given( $callable = function () { }, $this->function->register_shutdown_function = function ($_callable) use (&$called, $self, &$callable) { $called = true; $self ->variable($_callable) ->isEqualTo($callable); return true; } ) ->when($result = SUT::registerShutdownFunction($callable)) ->then ->boolean($result) ->isTrue(); } public function case_get_php_binary_with_constant() { $this ->given($this->constant->PHP_BINARY = '/foo/php') ->when($result = SUT::getPHPBinary()) ->then ->string($result) ->isEqualTo('/foo/php'); } public function case_get_php_binary_with_server() { $this ->given( $this->function->defined = false, $_SERVER['_'] = '/bar/php' ) ->when($result = SUT::getPHPBinary()) ->then ->string($result) ->isEqualTo('/bar/php'); } public function case_get_php_binary_with_bin_directory() { unset($_SERVER['_']); $this ->given( $this->function->defined = false, $this->function->file_exists = true, $this->function->realpath = '/baz/php' ) ->when($result = SUT::getPHPBinary()) ->then ->string($result) ->isEqualTo('/baz/php'); } public function case_uuid() { $this ->given($this->function->mt_rand = 42) ->when($result = SUT::uuid()) ->then ->string($result) ->isEqualTo('002a002a-002a-402a-802a-002a002a002a'); } public function case_uuid_all_differents() { $this ->when(function () { $uuids = []; for ($i = 0; $i < 10000; ++$i) { $uuids[] = SUT::uuid(); } $this ->integer(count($uuids)) ->isEqualTo(count(array_unique($uuids))); }); } } when($result = new SUT('foo', 0)) ->then ->object($result) ->isInstanceOf('Hoa\Exception\Exception'); } } when($result = new SUT('strtoupper')) ->then ->string($result('foo')) ->isEqualTo('FOO') ->string($result->getValidCallback()) ->isEqualTo('strtoupper') ->string($result->getHash()) ->isEqualTo('function#strtoupper') ->isEqualTo($result . '') ->object($reflection = $result->getReflection()) ->isInstanceOf('ReflectionFunction') ->string($reflection->getName()) ->isEqualTo('strtoupper'); } public function case_form_class___method() { $this ->when($result = new SUT(__CLASS__ . '::strtoupper')) ->then ->string($result('foo')) ->isEqualTo('FOO') ->array($result->getValidCallback()) ->isEqualTo([__CLASS__, 'strtoupper']) ->string($result->getHash()) ->isEqualTo('class#' . __CLASS__ . '::strtoupper') ->isEqualTo($result . '') ->object($reflection = $result->getReflection()) ->isInstanceOf('ReflectionMethod') ->string($reflection->getName()) ->isEqualTo('strtoupper'); } public function case_form_class_method() { $this ->when($result = new SUT(__CLASS__, 'strtoupper')) ->then ->string($result('foo')) ->isEqualTo('FOO') ->array($result->getValidCallback()) ->isEqualTo([__CLASS__, 'strtoupper']) ->string($result->getHash()) ->isEqualTo('class#' . __CLASS__ . '::strtoupper') ->isEqualTo($result . '') ->object($reflection = $result->getReflection()) ->isInstanceOf('ReflectionMethod') ->string($reflection->getName()) ->isEqualTo('strtoupper'); } public function case_form_object_method() { $this ->when($result = new SUT($this, 'strtolower')) ->then ->string($result('FOO')) ->isEqualTo('foo') ->array($result->getValidCallback()) ->isEqualTo([$this, 'strtolower']) ->string($result->getHash()) ->matches( '/^object\([^:]+\)#' . preg_quote(__CLASS__) . '::strtolower$/' ) ->isEqualTo($result . '') ->object($reflection = $result->getReflection()) ->isInstanceOf('ReflectionMethod') ->string($reflection->getName()) ->isEqualTo('strtolower'); } public function case_form_object_invoke() { $this ->when($result = new SUT($this)) ->then ->string($result('foo')) ->isEqualTo('FOO') ->array($result->getValidCallback()) ->isEqualTo([$this, '__invoke']) ->string($result->getHash()) ->matches( '/^object\([^:]+\)#' . preg_quote(__CLASS__) . '::__invoke$/' ) ->isEqualTo($result . '') ->object($reflection = $result->getReflection()) ->isInstanceOf('ReflectionMethod') ->string($reflection->getName()) ->isEqualTo('__invoke'); } public function case_form_closure() { $this ->given( $closure = function ($string) { return strtoupper($string); } ) ->when($result = new SUT($closure)) ->then ->string($result('foo')) ->isEqualTo('FOO') ->object($result->getValidCallback()) ->isIdenticalTo($closure) ->string($result->getHash()) ->matches('/^closure\([^:]+\)$/') ->isEqualTo($result . '') ->object($reflection = $result->getReflection()) ->isInstanceOf('ReflectionFunction') ->string($reflection->getName()) ->isEqualTo('Hoa\Consistency\Test\Unit\{closure}'); } public function case_form_array_of_class_method() { $this ->when($result = new SUT([__CLASS__, 'strtoupper'])) ->then ->string($result('foo')) ->isEqualTo('FOO') ->array($result->getValidCallback()) ->isEqualTo([__CLASS__, 'strtoupper']) ->string($result->getHash()) ->isEqualTo('class#' . __CLASS__ . '::strtoupper') ->isEqualTo($result . '') ->object($reflection = $result->getReflection()) ->isInstanceOf('ReflectionMethod') ->string($reflection->getName()) ->isEqualTo('strtoupper'); } public function case_form_array_of_object_method() { $this ->when($result = new SUT([$this, 'strtolower'])) ->then ->string($result('FOO')) ->isEqualTo('foo') ->array($result->getValidCallback()) ->isEqualTo([$this, 'strtolower']) ->string($result->getHash()) ->matches( '/^object\([^:]+\)#' . preg_quote(__CLASS__) . '::strtolower$/' ) ->isEqualTo($result . '') ->object($reflection = $result->getReflection()) ->isInstanceOf('ReflectionMethod') ->string($reflection->getName()) ->isEqualTo('strtolower'); } public function case_form_able_not_a_string() { $this ->exception(function () { new SUT(__CLASS__, 123); }) ->isInstanceOf('Hoa\Consistency\Exception'); } public function case_form_function_not_defined() { $this ->exception(function () { new SUT('__hoa_test_undefined_function__'); }) ->isInstanceOf('Hoa\Consistency\Exception'); } public function case_form_able_cannot_be_deduced() { $this ->given($this->function->method_exists = false) ->exception(function () { new SUT($this); }) ->isInstanceOf('Hoa\Consistency\Exception'); } public function case_invoke() { $this ->given( $callable = new SUT( function ($x, $y, $z) { return [$x, $y, $z]; } ) ) ->when($result = $callable(7, [4.2], 'foo')) ->then ->array($result) ->isEqualTo([7, [4.2], 'foo']); } public function case_distribute_arguments() { $this ->given( $callable = new SUT( function ($x, $y, $z) { return [$x, $y, $z]; } ) ) ->when($result = $callable->distributeArguments([7, [4.2], 'foo'])) ->then ->array($result) ->isEqualTo([7, [4.2], 'foo']); } protected function _get_valid_callback_stream_xxx($argument, $method) { $this ->given( $stream = new \Mock\Hoa\Stream\IStream\Out(), $arguments = [$argument], $xcallable = new SUT($stream) ) ->when($result = $xcallable->getValidCallback($arguments)) ->then ->array($result) ->isEqualTo([$stream, $method]); } public function case_get_valid_callback_stream_character() { return $this->_get_valid_callback_stream_xxx('f', 'writeCharacter'); } public function case_get_valid_callback_stream_string() { return $this->_get_valid_callback_stream_xxx('foo', 'writeString'); } public function case_get_valid_callback_stream_boolean() { return $this->_get_valid_callback_stream_xxx(true, 'writeBoolean'); } public function case_get_valid_callback_stream_integer() { return $this->_get_valid_callback_stream_xxx(7, 'writeInteger'); } public function case_get_valid_callback_stream_array() { return $this->_get_valid_callback_stream_xxx([4, 2], 'writeArray'); } public function case_get_valid_callback_stream_float() { return $this->_get_valid_callback_stream_xxx(4.2, 'writeFloat'); } public function case_get_valid_callback_stream_other() { return $this->_get_valid_callback_stream_xxx($this, 'writeAll'); } public static function strtoupper($string) { return strtoupper($string); } public function strtolower($string) { return strtolower($string); } public function __invoke($string) { return strtoupper($string); } public function __toString() { return 'hello'; } } _callback = $call; return; } if (!is_string($able)) { throw new Exception( 'Bad callback form; the able part must be a string.', 0 ); } if ('' === $able) { if (is_string($call)) { if (false === strpos($call, '::')) { if (!function_exists($call)) { throw new Exception( 'Bad callback form; function %s does not exist.', 1, $call ); } $this->_callback = $call; return; } list($call, $able) = explode('::', $call); } elseif (is_object($call)) { if ($call instanceof Stream\IStream\Out) { $able = null; } elseif (method_exists($call, '__invoke')) { $able = '__invoke'; } else { throw new Exception( 'Bad callback form; an object but without a known ' . 'method.', 2 ); } } elseif (is_array($call) && isset($call[0])) { if (!isset($call[1])) { return $this->__construct($call[0]); } return $this->__construct($call[0], $call[1]); } else { throw new Exception( 'Bad callback form.', 3 ); } } $this->_callback = [$call, $able]; return; } public function __invoke() { $arguments = func_get_args(); $valid = $this->getValidCallback($arguments); return call_user_func_array($valid, $arguments); } public function distributeArguments(array $arguments) { return call_user_func_array([$this, '__invoke'], $arguments); } public function getValidCallback(array &$arguments = []) { $callback = $this->_callback; $head = null; if (isset($arguments[0])) { $head = &$arguments[0]; } if (null !== $head && is_array($callback) && null === $callback[1]) { if ($head instanceof Event\Bucket) { $head = $head->getData(); } switch ($type = gettype($head)) { case 'string': if (1 === strlen($head)) { $method = 'writeCharacter'; } else { $method = 'writeString'; } break; case 'boolean': case 'integer': case 'array': $method = 'write' . ucfirst($type); break; case 'double': $method = 'writeFloat'; break; default: $method = 'writeAll'; $head = $head . "\n"; } $callback[1] = $method; } return $callback; } public function getHash() { if (null !== $this->_hash) { return $this->_hash; } $_ = &$this->_callback; if (is_string($_)) { return $this->_hash = 'function#' . $_; } if (is_array($_)) { return $this->_hash = (is_object($_[0]) ? 'object(' . spl_object_hash($_[0]) . ')' . '#' . get_class($_[0]) : 'class#' . $_[0]) . '::' . (null !== $_[1] ? $_[1] : '???'); } return $this->_hash = 'closure(' . spl_object_hash($_) . ')'; } public function getReflection() { $arguments = func_get_args(); $valid = $this->getValidCallback($arguments); if (is_string($valid)) { return new \ReflectionFunction($valid); } if ($valid instanceof \Closure) { return new \ReflectionFunction($valid); } if (is_array($valid)) { if (is_string($valid[0])) { if (false === method_exists($valid[0], $valid[1])) { return new \ReflectionClass($valid[0]); } return new \ReflectionMethod($valid[0], $valid[1]); } $object = new \ReflectionObject($valid[0]); if (null === $valid[1]) { return $object; } return $object->getMethod($valid[1]); } } public function __toString() { return $this->getHash(); } } getOption($v)) { switch ($c) { case 't': echo $tput->getTerm(); return; case 'f': echo $tput->getTerminfo(); return; case 'H': echo $tput->has($v) ? 1 : 0; return; case 'c': echo $tput->count($v); return; case 'g': echo $tput->get($v); return; case 'b': $informations = $tput->getInformations(); static::format($informations['booleans']); return; case 'n': $informations = $tput->getInformations(); static::format($informations['numbers']); return; case 's': $informations = $tput->getInformations(); static::format($informations['strings']); return; case '__ambiguous': $this->resolveOptionAmbiguity($v); break; case 'h': case '?': default: return $this->usage(); } } return $this->usage(); } public function usage() { echo 'Usage : console:termcap', "\n", 'Options :', "\n", $this->makeUsageOptionsList([ 't' => 'Get terminal name.', 'f' => 'Get path to the terminfo file.', 'H' => 'Get value of a boolean capability.', 'c' => 'Get value of a number capability.', 'g' => 'Get value of a string capability.', 'b' => 'Get all boolean capabilites.', 'n' => 'Get all number capabilites.', 's' => 'Get all string capabilites.', 'help' => 'This help.' ]), "\n", 'Examples:', "\n", ' $ hoa console:termcap --count max_colors', "\n", ' $ TERM=vt200 hoa console:termcap --has back_color_erase', "\n"; return; } public static function format(array $data) { $max = 0; foreach ($data as $key => $_) { if ($max < ($handle = strlen($key))) { $max = $handle; } } $format = '%-' . ($max + 1) . 's: %s' . "\n"; foreach ($data as $key => $value) { printf( $format, $key, is_bool($value) ? ($value ? 'true' : 'false') : (is_string($value) ? str_replace( [ "\033", "\n", ord(0xa), "\r", "\b", "\f", "\0" ], [ '\e', '\n', '\l', '\r', '\b', '\f', '\0' ], $value ) : $value) ); } return; } } __halt_compiler(); Terminal capabilities. `tty` < `tty`', false ); } } open(); } $process->writeAll($output); if ($mode & PHP_OUTPUT_HANDLER_FINAL) { $process->close(); } return null; } } writeAll($text); Console\Cursor::colorize($attributesAfter); $out = ob_get_contents(); ob_end_clean(); return $out; } public static function columnize( array $line, $alignement = self::ALIGN_LEFT, $horizontalPadding = 2, $verticalPadding = 0, $separator = null ) { if (empty($line)) { return ''; } $separator = explode('|', $separator); $nbColumn = 0; $nbLine = count($line); $xtraWidth = 2 * ($verticalPadding + 2); foreach ($line as $key => &$column) { if (!is_array($column)) { $column = [$column]; } $handle = count($column); $handle > $nbColumn and $nbColumn = $handle; } $xtraWidth += $horizontalPadding * $nbColumn; $columnWidth = array_fill(0, $nbColumn, 0); for ($e = 0; $e < $nbColumn; $e++) { for ($i = 0; $i < $nbLine; $i++) { if (!isset($line[$i][$e])) { continue; } $handle = self::getMaxLineWidth($line[$i][$e]); $handle > $columnWidth[$e] and $columnWidth[$e] = $handle; } } $window = Console\Window::getSize(); $envWindow = $window['x']; while ($envWindow <= ($cWidthSum = $xtraWidth + array_sum($columnWidth))) { $diff = $cWidthSum - $envWindow; $max = max($columnWidth) - $xtraWidth; $newWidth = $max - $diff; $i = array_search(max($columnWidth), $columnWidth); $columnWidth[$i] = $newWidth; foreach ($line as $key => &$c) { if (isset($c[$i])) { $c[$i] = self::wordwrap($c[$i], $newWidth); } } } $columnWidth = array_map( function ($x) use ($horizontalPadding) { return $x + 2 * $horizontalPadding; }, $columnWidth ); $newLine = []; foreach ($line as $key => $plpl) { $i = self::getMaxLineNumber($plpl); while ($i-- >= 0) { $newLine[] = array_fill(0, $nbColumn, null); } } $yek = 0; foreach ($line as $key => $col) { foreach ($col as $kkey => $value) { if (false === strpos($value, "\n")) { $newLine[$yek][$kkey] = $value; continue; } foreach (explode("\n", $value) as $foo => $oof) { $newLine[$yek + $foo][$kkey] = $oof; } } $i = self::getMaxLineNumber($col); $i > 0 and $yek += $i; $yek++; } foreach ($newLine as $key => $col) { foreach ($col as $kkey => $value) { if (isset($separator[$kkey])) { $newLine[$key][$kkey] = $separator[$kkey] . str_replace( "\n", "\n" . $separator[$kkey], $value ); } } } $line = $newLine; unset($newLine); foreach ($line as $key => &$column) { $handle = count($column); if ($nbColumn - $handle > 0) { $column += array_fill($handle, $nbColumn - $handle, null); } } $out = null; $dash = $alignement === self::ALIGN_LEFT ? '-' : ''; foreach ($line as $key => $handle) { $format = null; foreach ($handle as $i => $hand) { if (preg_match_all('#(\\e\[[0-9]+m)#', $hand, $match)) { $a = $columnWidth[$i]; foreach ($match as $m) { $a += strlen($m[1]); } $format .= '%' . $dash . ($a + floor(count($match) / 2)) . 's'; } else { $format .= '%' . $dash . $columnWidth[$i] . 's'; } } $format .= str_repeat("\n", $verticalPadding + 1); array_unshift($handle, $format); $out .= call_user_func_array('sprintf', $handle); } return $out; } public static function align( $text, $alignement = self::ALIGN_LEFT, $width = null ) { if (null === $width) { $window = Console\Window::getSize(); $width = $window['x']; } $out = null; switch ($alignement) { case self::ALIGN_LEFT: $out .= sprintf('%-' . $width . 's', self::wordwrap($text, $width)); break; case self::ALIGN_CENTER: foreach (explode("\n", self::wordwrap($text, $width)) as $key => $value) { $out .= str_repeat(' ', ceil(($width - strlen($value)) / 2)) . $value . "\n"; } break; case self::ALIGN_RIGHT: default: foreach (explode("\n", self::wordwrap($text, $width)) as $key => $value) { $out .= sprintf('%' . $width . 's' . "\n", $value); } break; } return $out; } protected static function getMaxLineWidth($lines) { if (!is_array($lines)) { $lines = [$lines]; } $width = 0; foreach ($lines as $foo => $line) { foreach (explode("\n", $line) as $fooo => $lin) { $lin = preg_replace('#\\e\[[0-9]+m#', '', $lin); strlen($lin) > $width and $width = strlen($lin); } } return $width; } protected static function getMaxLineNumber($lines) { if (!is_array($lines)) { $lines = [$lines]; } $number = 0; foreach ($lines as $foo => $line) { substr_count($line, "\n") > $number and $number = substr_count($line, "\n"); } return $number; } public static function wordwrap($text, $width = null, $break = "\n") { if (null === $width) { $window = Console\Window::getSize(); $width = $window['x']; } return wordwrap($text, $width, $break, true); } public static function underline($text, $pattern = '*') { $text = explode("\n", $text); $card = strlen($pattern); foreach ($text as $key => &$value) { $i = -1; $max = strlen($value); while ($value{++$i} == ' ' && $i < $max); $underline = str_repeat(' ', $i) . str_repeat($pattern, strlen(trim($value)) / $card) . str_repeat(' ', strlen($value) - $i - strlen(trim($value))); $value .= "\n" . $underline; } return implode("\n", $text); } } $repeat) { return; } elseif (1 === $repeat) { $handle = explode(' ', $steps); } else { $handle = explode(' ', $steps, 1); } $tput = Console::getTput(); $output = Console::getOutput(); foreach ($handle as $step) { switch ($step) { case 'u': case 'up': case '↑': $output->writeAll( str_replace( '%p1%d', $repeat, $tput->get('parm_up_cursor') ) ); break; case 'U': case 'UP': static::moveTo(null, 1); break; case 'r': case 'right': case '→': $output->writeAll( str_replace( '%p1%d', $repeat, $tput->get('parm_right_cursor') ) ); break; case 'R': case 'RIGHT': static::moveTo(9999); break; case 'd': case 'down': case '↓': $output->writeAll( str_replace( '%p1%d', $repeat, $tput->get('parm_down_cursor') ) ); break; case 'D': case 'DOWN': static::moveTo(null, 9999); break; case 'l': case 'left': case '←': $output->writeAll( str_replace( '%p1%d', $repeat, $tput->get('parm_left_cursor') ) ); break; case 'L': case 'LEFT': static::moveTo(1); break; } } return; } public static function moveTo($x = null, $y = null) { if (null === $x || null === $y) { $position = static::getPosition(); if (null === $x) { $x = $position['x']; } if (null === $y) { $y = $position['y']; } } Console::getOutput()->writeAll( str_replace( ['%i%p1%d', '%p2%d'], [$y, $x], Console::getTput()->get('cursor_address') ) ); return; } public static function getPosition() { $tput = Console::getTput(); $user7 = $tput->get('user7'); if (null === $user7) { return [ 'x' => 0, 'y' => 0 ]; } Console::getOutput()->writeAll($user7); $input = Console::getInput(); $input->read(2); $x = null; $y = null; $handle = &$y; do { $char = $input->readCharacter(); switch ($char) { case ';': $handle = &$x; break; case 'R': break 2; default: $handle .= $char; } } while (true); return [ 'x' => (int) $x, 'y' => (int) $y ]; } public static function save() { Console::getOutput()->writeAll( Console::getTput()->get('save_cursor') ); return; } public static function restore() { Console::getOutput()->writeAll( Console::getTput()->get('restore_cursor') ); return; } public static function clear($parts = 'all') { $tput = Console::getTput(); $output = Console::getOutput(); foreach (explode(' ', $parts) as $part) { switch ($part) { case 'a': case 'all': case '↕': $output->writeAll($tput->get('clear_screen')); static::moveTo(1, 1); break; case 'u': case 'up': case '↑': $output->writeAll("\033[1J"); break; case 'r': case 'right': case '→': $output->writeAll($tput->get('clr_eol')); break; case 'd': case 'down': case '↓': $output->writeAll($tput->get('clr_eos')); break; case 'l': case 'left': case '←': $output->writeAll($tput->get('clr_bol')); break; case 'line': case '↔': $output->writeAll("\r" . $tput->get('clr_eol')); break; } } return; } public static function hide() { Console::getOutput()->writeAll( Console::getTput()->get('cursor_invisible') ); return; } public static function show() { Console::getOutput()->writeAll( Console::getTput()->get('cursor_visible') ); return; } public static function colorize($attributes) { static $_rgbTo256 = null; if (null === $_rgbTo256) { $_rgbTo256 = [ '000000', '800000', '008000', '808000', '000080', '800080', '008080', 'c0c0c0', '808080', 'ff0000', '00ff00', 'ffff00', '0000ff', 'ff00ff', '00ffff', 'ffffff', '000000', '00005f', '000087', '0000af', '0000d7', '0000ff', '005f00', '005f5f', '005f87', '005faf', '005fd7', '005fff', '008700', '00875f', '008787', '0087af', '0087d7', '0087ff', '00af00', '00af5f', '00af87', '00afaf', '00afd7', '00afff', '00d700', '00d75f', '00d787', '00d7af', '00d7d7', '00d7ff', '00ff00', '00ff5f', '00ff87', '00ffaf', '00ffd7', '00ffff', '5f0000', '5f005f', '5f0087', '5f00af', '5f00d7', '5f00ff', '5f5f00', '5f5f5f', '5f5f87', '5f5faf', '5f5fd7', '5f5fff', '5f8700', '5f875f', '5f8787', '5f87af', '5f87d7', '5f87ff', '5faf00', '5faf5f', '5faf87', '5fafaf', '5fafd7', '5fafff', '5fd700', '5fd75f', '5fd787', '5fd7af', '5fd7d7', '5fd7ff', '5fff00', '5fff5f', '5fff87', '5fffaf', '5fffd7', '5fffff', '870000', '87005f', '870087', '8700af', '8700d7', '8700ff', '875f00', '875f5f', '875f87', '875faf', '875fd7', '875fff', '878700', '87875f', '878787', '8787af', '8787d7', '8787ff', '87af00', '87af5f', '87af87', '87afaf', '87afd7', '87afff', '87d700', '87d75f', '87d787', '87d7af', '87d7d7', '87d7ff', '87ff00', '87ff5f', '87ff87', '87ffaf', '87ffd7', '87ffff', 'af0000', 'af005f', 'af0087', 'af00af', 'af00d7', 'af00ff', 'af5f00', 'af5f5f', 'af5f87', 'af5faf', 'af5fd7', 'af5fff', 'af8700', 'af875f', 'af8787', 'af87af', 'af87d7', 'af87ff', 'afaf00', 'afaf5f', 'afaf87', 'afafaf', 'afafd7', 'afafff', 'afd700', 'afd75f', 'afd787', 'afd7af', 'afd7d7', 'afd7ff', 'afff00', 'afff5f', 'afff87', 'afffaf', 'afffd7', 'afffff', 'd70000', 'd7005f', 'd70087', 'd700af', 'd700d7', 'd700ff', 'd75f00', 'd75f5f', 'd75f87', 'd75faf', 'd75fd7', 'd75fff', 'd78700', 'd7875f', 'd78787', 'd787af', 'd787d7', 'd787ff', 'd7af00', 'd7af5f', 'd7af87', 'd7afaf', 'd7afd7', 'd7afff', 'd7d700', 'd7d75f', 'd7d787', 'd7d7af', 'd7d7d7', 'd7d7ff', 'd7ff00', 'd7ff5f', 'd7ff87', 'd7ffaf', 'd7ffd7', 'd7ffff', 'ff0000', 'ff005f', 'ff0087', 'ff00af', 'ff00d7', 'ff00ff', 'ff5f00', 'ff5f5f', 'ff5f87', 'ff5faf', 'ff5fd7', 'ff5fff', 'ff8700', 'ff875f', 'ff8787', 'ff87af', 'ff87d7', 'ff87ff', 'ffaf00', 'ffaf5f', 'ffaf87', 'ffafaf', 'ffafd7', 'ffafff', 'ffd700', 'ffd75f', 'ffd787', 'ffd7af', 'ffd7d7', 'ffd7ff', 'ffff00', 'ffff5f', 'ffff87', 'ffffaf', 'ffffd7', 'ffffff', '080808', '121212', '1c1c1c', '262626', '303030', '3a3a3a', '444444', '4e4e4e', '585858', '606060', '666666', '767676', '808080', '8a8a8a', '949494', '9e9e9e', 'a8a8a8', 'b2b2b2', 'bcbcbc', 'c6c6c6', 'd0d0d0', 'dadada', 'e4e4e4', 'eeeeee' ]; } $tput = Console::getTput(); if (1 >= $tput->count('max_colors')) { return; } $handle = []; foreach (explode(' ', $attributes) as $attribute) { switch ($attribute) { case 'n': case 'normal': $handle[] = 0; break; case 'b': case 'bold': $handle[] = 1; break; case 'u': case 'underlined': $handle[] = 4; break; case 'bl': case 'blink': $handle[] = 5; break; case 'i': case 'inverse': $handle[] = 7; break; case '!b': case '!bold': $handle[] = 22; break; case '!u': case '!underlined': $handle[] = 24; break; case '!bl': case '!blink': $handle[] = 25; break; case '!i': case '!inverse': $handle[] = 27; break; default: if (0 === preg_match('#^([^\(]+)\(([^\)]+)\)$#', $attribute, $m)) { break; } $shift = 0; switch ($m[1]) { case 'fg': case 'foreground': $shift = 0; break; case 'bg': case 'background': $shift = 10; break; default: break 2; } $_handle = 0; $_keyword = true; switch ($m[2]) { case 'black': $_handle = 30; break; case 'red': $_handle = 31; break; case 'green': $_handle = 32; break; case 'yellow': $_handle = 33; break; case 'blue': $_handle = 34; break; case 'magenta': $_handle = 35; break; case 'cyan': $_handle = 36; break; case 'white': $_handle = 37; break; case 'default': $_handle = 39; break; default: $_keyword = false; if (256 <= $tput->count('max_colors') && '#' === $m[2][0]) { $rgb = hexdec(substr($m[2], 1)); $r = ($rgb >> 16) & 255; $g = ($rgb >> 8) & 255; $b = $rgb & 255; $distance = null; foreach ($_rgbTo256 as $i => $_rgb) { $_rgb = hexdec($_rgb); $_r = ($_rgb >> 16) & 255; $_g = ($_rgb >> 8) & 255; $_b = $_rgb & 255; $d = sqrt( pow($_r - $r, 2) + pow($_g - $g, 2) + pow($_b - $b, 2) ); if (null === $distance || $d <= $distance) { $distance = $d; $_handle = $i; } } } else { $_handle = intval($m[2]); } } if (true === $_keyword) { $handle[] = $_handle + $shift; } else { $handle[] = (38 + $shift) . ';5;' . $_handle; } } } Console::getOutput()->writeAll("\033[" . implode(';', $handle) . "m"); return; } public static function changeColor($fromCode, $toColor) { $tput = Console::getTput(); if (true !== $tput->has('can_change')) { return; } $r = ($toColor >> 16) & 255; $g = ($toColor >> 8) & 255; $b = $toColor & 255; Console::getOutput()->writeAll( str_replace( [ '%p1%d', 'rgb:', '%p2%{255}%*%{1000}%/%2.2X/', '%p3%{255}%*%{1000}%/%2.2X/', '%p4%{255}%*%{1000}%/%2.2X' ], [ $fromCode, '', sprintf('%02x', $r), sprintf('%02x', $g), sprintf('%02x', $b) ], $tput->get('initialize_color') ) ); return; } public static function setStyle($style, $blink = true) { if (OS_WIN) { return; } switch ($style) { case 'b': case 'block': case '▋': $_style = 1; break; case 'u': case 'underline': case '_': $_style = 2; break; case 'v': case 'vertical': case '|': $_style = 5; break; } if (false === $blink) { ++$_style; } Console::getOutput()->writeAll("\033[" . $_style . " q"); return; } public static function bip() { Console::getOutput()->writeAll( Console::getTput()->get('bell') ); return; } } Console::advancedInteraction(); parser = new Console\Parser(); return; } public function getOption(&$optionValue, $short = null) { if (null === $this->_options && !empty($this->options)) { $this->setOptions($this->options); } if (null === $this->_options) { return false; } return $this->_options->getOption($optionValue, $short); } public function setOptions(array $options) { $old = $this->options; $this->options = $options; $rule = $this->router->getTheRule(); $variables = $rule[Router::RULE_VARIABLES]; if (isset($variables['_tail'])) { $this->parser->parse($variables['_tail']); $this->_options = new Console\GetOption( $this->options, $this->parser ); } return $old; } public function makeUsageOptionsList(array $definitions = []) { $out = []; foreach ($this->options as $i => $options) { $out[] = [ ' -' . $options[Console\GetOption::OPTION_VAL] . ', --' . $options[Console\GetOption::OPTION_NAME] . ($options[Console\GetOption::OPTION_HAS_ARG] === Console\GetOption::REQUIRED_ARGUMENT ? '=' : ($options[Console\GetOption::OPTION_HAS_ARG] === Console\GetOption::OPTIONAL_ARGUMENT ? '[=]' : '')), (isset($definitions[$options[Console\GetOption::OPTION_VAL]]) ? $definitions[$options[Console\GetOption::OPTION_VAL]] : (isset($definitions[$options[0]]) ? $definitions[$options[Console\GetOption::OPTION_NAME]] : null ) ) ]; } return Console\Chrome\Text::columnize( $out, Console\Chrome\Text::ALIGN_LEFT, .5, 0, '|: ' ); } public function resolveOptionAmbiguity(array $solutions) { echo 'You have made a typo in the option ', $solutions['option'], '; it can match the following options: ', "\n", ' • ', implode(";\n • ", $solutions['solutions']), '.', "\n", 'Please, type the right option (empty to choose the first one):', "\n"; $new = $this->readLine('> '); if (empty($new)) { $new = $solutions['solutions'][0]; } $solutions['solutions'] = [$new]; $this->_options->resolveOptionAmbiguity($solutions); return; } public function status($text, $status) { $window = Console\Window::getSize(); $out = ' ' . Console\Chrome\Text::colorize('*', 'foreground(yellow)') . ' ' . $text . str_pad( ' ', $window['x'] - strlen(preg_replace('#' . "\033" . '\[[0-9]+m#', '', $text)) - 8 ) . ($status === true ? '[' . Console\Chrome\Text::colorize('ok', 'foreground(green)') . ']' : '[' . Console\Chrome\Text::colorize('!!', 'foreground(white) background(red)') . ']'); Console::getOutput()->writeAll($out . "\n"); return; } public function readLine($prefix = null) { static $_rl = null; if (null === $_rl) { $_rl = new Console\Readline(); } return $_rl->readLine($prefix); } public function readPassword($prefix = null) { static $_rl = null; if (null === $_rl) { $_rl = new Console\Readline\Password(); } return $_rl->readLine($prefix); } } _options = $options; $this->_parser = $parser; if (empty($options)) { $this->_pipette[null] = null; return; } $names = []; foreach ($options as $i => $option) { if (isset($option[self::OPTION_NAME])) { $names[$option[self::OPTION_NAME]] = $i; } if (isset($option[self::OPTION_VAL])) { $names[$option[self::OPTION_VAL]] = $i; } } $_names = array_keys($names); $switches = $parser->getSwitches(); foreach ($switches as $name => $value) { if (false === in_array($name, $_names)) { if (1 === strlen($name)) { $this->_pipette[] = ['__ambiguous', [ 'solutions' => [], 'value' => $value, 'option' => $name ]]; continue; } $haystack = implode(';', $_names); $differences = (int) ceil(strlen($name) / 3); $searched = Ustring\Search::approximated( $haystack, $name, $differences ); $solutions = []; foreach ($searched as $s) { $h = substr($haystack, $s['i'], $s['l']); if (false !== strpos($h, ';') || false !== in_array($h, array_keys($switches)) || false === in_array($h, $_names)) { continue; } $solutions[] = $h; } if (empty($solutions)) { continue; } $this->_pipette[] = ['__ambiguous', [ 'solutions' => $solutions, 'value' => $value, 'option' => $name ]]; continue; } $option = $options[$names[$name]]; $argument = $option[self::OPTION_HAS_ARG]; if (self::NO_ARGUMENT === $argument) { if (!is_bool($value)) { $parser->transferSwitchToInput($name, $value); } } elseif (self::REQUIRED_ARGUMENT === $argument && !is_string($value)) { throw new Exception( 'The argument %s requires a value (it is not a switch).', 0, $name ); } $this->_pipette[] = [$option[self::OPTION_VAL], $value]; } $this->_pipette[null] = null; reset($this->_pipette); return; } public function getOption(&$optionValue, $short = null) { static $first = true; $optionValue = null; if (true === $this->isPipetteEmpty() && true === $first) { $first = false; return false; } $k = key($this->_pipette); $c = current($this->_pipette); $key = $c[0]; $value = $c[1]; if (null == $k && null === $c) { reset($this->_pipette); $first = true; return false; } $allow = []; if (null === $short) { foreach ($this->_options as $option) { $allow[] = $option[self::OPTION_VAL]; } } else { $allow = str_split($short); } if (!in_array($key, $allow) && '__ambiguous' != $key) { return false; } $optionValue = $value; $return = $key; next($this->_pipette); return $return; } public function isPipetteEmpty() { return count($this->_pipette) == 1; } public function resolveOptionAmbiguity(array $solutions) { if (!isset($solutions['solutions']) || !isset($solutions['value']) || !isset($solutions['option'])) { throw new Exception( 'Cannot resolve option ambiguity because the given solution ' . 'seems to be corruped.', 1 ); } $choices = $solutions['solutions']; if (1 > count($choices)) { throw new Exception( 'Cannot resolve ambiguity, fix your typo in the option %s :-).', 2, $solutions['option'] ); } $theSolution = $choices[0]; foreach ($this->_options as $option) { if ($theSolution == $option[self::OPTION_NAME] || $theSolution == $option[self::OPTION_VAL]) { $argument = $option[self::OPTION_HAS_ARG]; $value = $solutions['value']; if (self::NO_ARGUMENT === $argument) { if (!is_bool($value)) { $this->_parser->transferSwitchToInput($theSolution, $value); } } elseif (self::REQUIRED_ARGUMENT === $argument && !is_string($value)) { throw new Exception( 'The argument %s requires a value (it is not a switch).', 3, $theSolution ); } unset($this->_pipette[null]); $this->_pipette[] = [$option[self::OPTION_VAL], $value]; $this->_pipette[null] = null; return; } } return; } } _input = $input; return; } public function getStream() { return $this->_input; } public function eof() { return $this->_input->eof(); } public function read($length) { return $this->_input->read($length); } public function readString($length) { return $this->_input->readString($length); } public function readCharacter() { return $this->_input->readCharacter(); } public function readBoolean() { return $this->_input->readBoolean(); } public function readInteger($length = 1) { return $this->_input->readInteger($length); } public function readFloat($length = 1) { return $this->_input->readFloat($length); } public function readArray($argument = null) { return $this->_input->readArray($argument); } public function readLine() { return $this->_input->readLine(); } public function readAll($offset = 0) { return $this->_input->readAll($offset); } public function scanf($format) { return $this->_input->scanf($format); } } setListener( new Event\Listener( $this, [ 'mouseup', 'mousedown', 'wheelup', 'wheeldown', ] ) ); return; } public static function getInstance() { if (null === static::$_instance) { static::$_instance = new static(); } return static::$_instance; } public static function track() { if (true === static::$_enabled) { return; } static::$_enabled = true; Console::getOutput()->writeAll( "\033[1;2'z" . "\033[?1000h" . "\033[?1003h" ); $instance = static::getInstance(); $bucket = [ 'x' => 0, 'y' => 0, 'button' => null, 'shift' => false, 'meta' => false, 'ctrl' => false ]; $input = Console::getInput(); $read = [$input->getStream()->getStream()]; while (true) { if (false === @stream_select($read, $write, $except, 30)) { static::untrack(); break; } $string = $input->readCharacter(); if ("\033" !== $string) { continue; } $char = $input->readCharacter(); if ('[' !== $char) { continue; } $char = $input->readCharacter(); if ('M' !== $char) { continue; } $data = $input->read(3); $cb = ord($data[0]); $cx = ord($data[1]) - 32; $cy = ord($data[2]) - 32; $bucket['x'] = $cx; $bucket['y'] = $cy; $bucket['shift'] = 0 !== ($cb & 4); $bucket['meta'] = 0 !== ($cb & 8); $bucket['ctrl'] = 0 !== ($cb & 16); $cb = ($cb | 28) ^ 28; $cb -= 32; switch ($cb) { case static::WHEEL_UP: $instance->getListener()->fire( 'wheelup', new Event\Bucket($bucket) ); break; case static::WHEEL_DOWN: $instance->getListener()->fire( 'wheeldown', new Event\Bucket($bucket) ); break; case static::BUTTON_RELEASE: $instance->getListener()->fire( 'mouseup', new Event\Bucket($bucket) ); $bucket['button'] = null; break; default: if (static::BUTTON_LEFT === $cb) { $bucket['button'] = 'left'; } elseif (static::BUTTON_MIDDLE === $cb) { $bucket['button'] = 'middle'; } elseif (static::BUTTON_RIGHT === $cb) { $bucket['button'] = 'right'; } else { continue 2; } $instance->getListener()->fire( 'mousedown', new Event\Bucket($bucket) ); } } return; } public static function untrack() { if (false === static::$_enabled) { return; } Console::getOutput()->writeAll( "\033[?1003l" . "\033[?1000l" ); static::$_enabled = false; return; } } Console::advancedInteraction(); Consistency::registerShutdownFunction(xcallable('Hoa\Console\Mouse::untrack')); _output = $output; return; } public function getStream() { return $this->_output; } public function write($string, $length) { if (0 > $length) { throw new Exception( 'Length must be greater than 0, given %d.', 0, $length ); } $out = substr($string, 0, $length); if (true === $this->isMultiplexerConsidered()) { if (true === Console::isTmuxRunning()) { $out = "\033Ptmux;" . str_replace("\033", "\033\033", $out) . "\033\\"; } $length = strlen($out); } if (null === $this->_output) { echo $out; } else { $this->_output->write($out, $length); } } public function writeString($string) { $string = (string) $string; return $this->write($string, strlen($string)); } public function writeCharacter($character) { return $this->write((string) $character[0], 1); } public function writeBoolean($boolean) { return $this->write(((bool) $boolean) ? '1' : '0', 1); } public function writeInteger($integer) { $integer = (string) (int) $integer; return $this->write($integer, strlen($integer)); } public function writeFloat($float) { $float = (string) (float) $float; return $this->write($float, strlen($float)); } public function writeArray(array $array) { $array = var_export($array, true); return $this->write($array, strlen($array)); } public function writeLine($line) { if (false === $n = strpos($line, "\n")) { return $this->write($line . "\n", strlen($line) + 1); } ++$n; return $this->write(substr($line, 0, $n), $n); } public function writeAll($string) { return $this->write($string, strlen($string)); } public function truncate($size) { return false; } public function considerMultiplexer($consider) { $old = $this->_considerMultiplexer; $this->_considerMultiplexer = $consider; return $old; } public function isMultiplexerConsidered() { return $this->_considerMultiplexer; } } _parsed); $this->_parsed = [ 'input' => [], 'switch' => [] ]; $regex = '#(?:(?--?[^=\s]+)(?:(?:(=)|(\s))(?(?(3)[^-]|).*?)(?(4)(?.*?)(?(6)(?addInput($match); } elseif (!isset($match['i']) && !isset($match['s'])) { if (isset($matches[$i + 1])) { $nextMatch = $matches[$i + 1]; if (!empty($nextMatch['i']) && '=' === $nextMatch['i'][0]) { ++$i; $match[2] = '='; $match[3] = $match[4] = null; $match['s'] = $match[5] = substr($nextMatch[7], 1); $this->addValuedSwitch($match); continue; } } $this->addBoolSwitch($match); } elseif (!isset($match['i']) && isset($match['s'])) { $this->addValuedSwitch($match); } } return; } protected function addInput(array $input) { $handle = $input['i']; if (!empty($input[6])) { $handle = str_replace('\\' . $input[6], $input[6], $handle); } else { $handle = str_replace('\\ ', ' ', $handle); } $this->_parsed['input'][] = $handle; return; } protected function addBoolSwitch(array $switch) { $this->addSwitch($switch['b'], true); return; } protected function addValuedSwitch(array $switch) { $this->addSwitch($switch['b'], $switch['s'], $switch[4]); return; } protected function addSwitch($name, $value, $escape = null) { if (substr($name, 0, 2) == '--') { return $this->addSwitch(substr($name, 2), $value, $escape); } if (substr($name, 0, 1) == '-') { if (true === $this->getLongOnly()) { return $this->addSwitch('-' . $name, $value, $escape); } foreach (str_split(substr($name, 1)) as $foo => $switch) { $this->addSwitch($switch, $value, $escape); } return; } if (null !== $escape) { $escape = '' == $escape ? ' ' : $escape; if (is_string($value)) { $value = str_replace('\\' . $escape, $escape, $value); } } elseif (is_string($value)) { $value = str_replace('\\ ', ' ', $value); } if (isset($this->_parsed['switch'][$name])) { if (is_bool($this->_parsed['switch'][$name])) { $value = !$this->_parsed['switch'][$name]; } else { $value = [$this->_parsed['switch'][$name], $value]; } } if (empty($name)) { return $this->addInput([6 => null, 'i' => $value]); } $this->_parsed['switch'][$name] = $value; return; } public function transferSwitchToInput($name, &$value) { if (!isset($this->_parsed['switch'][$name])) { return; } $this->_parsed['input'][] = $this->_parsed['switch'][$name]; $value = true; unset($this->_parsed['switch'][$name]); return; } public function getInputs() { return $this->_parsed['input']; } public function listInputs( &$a, &$b = null, &$c = null, &$d = null, &$e = null, &$f = null, &$g = null, &$h = null, &$i = null, &$j = null, &$k = null, &$l = null, &$m = null, &$n = null, &$o = null, &$p = null, &$q = null, &$r = null, &$s = null, &$t = null, &$u = null, &$v = null, &$w = null, &$x = null, &$y = null, &$z = null ) { $inputs = $this->getInputs(); $i = 'a'; $ii = -1; while (isset($inputs[++$ii]) && $i <= 'z') { ${$i++} = $inputs[$ii]; } return; } public function getSwitches() { return $this->_parsed['switch']; } public function parseSpecialValue($value, array $keywords = []) { $out = []; foreach (explode(',', $value) as $key => $subvalue) { $subvalue = str_replace( array_keys($keywords), array_values($keywords), $subvalue ); if (0 !== preg_match('#^(-?[0-9]+):(-?[0-9]+)$#', $subvalue, $matches)) { if (0 > $matches[1] && 0 > $matches[2]) { throw new Exception( 'Cannot give two negative numbers, given %s.', 0, $subvalue ); } array_shift($matches); $max = max($matches); $min = min($matches); if (0 > $max || 0 > $min) { if (0 > $max - $min) { throw new Exception( 'The difference between operands must be ' . 'positive.', 1 ); } $min = $max + $min; } $out = array_merge(range($min, $max), $out); } else { $out[] = $subvalue; } } return $out; } public function setLongOnly($longonly = false) { $old = $this->_longonly; $this->_longonly = $longonly; return $old; } public function getLongOnly() { return $this->_longonly; } } ['pipe', 'r'], 1 => ['pipe', 'w'], 2 => ['pipe', 'w'] ]; protected $_pipes = null; protected $_seekable = []; public function __construct( $command, array $options = null, array $descriptors = null, $cwd = null, array $environment = null, $timeout = 30 ) { $this->setCommand($command); if (null !== $options) { $this->setOptions($options); } if (null !== $descriptors) { $this->_descriptors = []; foreach ($descriptors as $descriptor => $nature) { if (isset($this->_descriptors[$descriptor])) { throw new Exception( 'Pipe descriptor %d already exists, cannot ' . 'redefine it.', 0, $descriptor ); } $this->_descriptors[$descriptor] = $nature; } } $this->setCwd($cwd ?: getcwd()); if (null !== $environment) { $this->setEnvironment($environment); } $this->setTimeout($timeout); parent::__construct($this->getCommandLine(), null, true); $this->getListener()->addIds(['input', 'output', 'timeout', 'start', 'stop']); return; } protected function &_open($streamName, Stream\Context $context = null) { $out = @proc_open( $streamName, $this->_descriptors, $this->_pipes, $this->getCwd(), $this->getEnvironment() ); if (false === $out) { throw new Exception( 'Something wrong happen when running %s.', 1, $streamName ); } return $out; } protected function _close() { foreach ($this->_pipes as $pipe) { @fclose($pipe); } return @proc_close($this->getStream()); } public function run() { if (false === $this->isOpened()) { $this->open(); } else { $this->_close(); $this->_setStream($this->_open( $this->getStreamName(), $this->getStreamContext() )); } $this->getListener()->fire('start', new Event\Bucket()); $_read = []; $_write = []; $_except = []; foreach ($this->_pipes as $p => $pipe) { switch ($this->_descriptors[$p][1]) { case 'r': stream_set_blocking($pipe, false); $_write[] = $pipe; break; case 'w': case 'a': stream_set_blocking($pipe, true); $_read[] = $pipe; break; } } while (true) { foreach ($_read as $i => $r) { if (false === is_resource($r)) { unset($_read[$i]); } } foreach ($_write as $i => $w) { if (false === is_resource($w)) { unset($_write[$i]); } } foreach ($_except as $i => $e) { if (false === is_resource($e)) { unset($_except[$i]); } } if (empty($_read) && empty($_write) && empty($_except)) { break; } $read = $_read; $write = $_write; $except = $_except; $select = stream_select($read, $write, $except, $this->getTimeout()); if (0 === $select) { $this->getListener()->fire('timeout', new Event\Bucket()); break; } foreach ($read as $i => $_r) { $pipe = array_search($_r, $this->_pipes); $line = $this->readLine($pipe); if (false === $line) { $result = [false]; } else { $result = $this->getListener()->fire( 'output', new Event\Bucket([ 'pipe' => $pipe, 'line' => $line ]) ); } if (true === feof($_r) || in_array(false, $result, true)) { fclose($_r); unset($_read[$i]); break; } } foreach ($write as $j => $_w) { $result = $this->getListener()->fire( 'input', new Event\Bucket([ 'pipe' => array_search($_w, $this->_pipes) ]) ); if (true === feof($_w) || in_array(false, $result, true)) { fclose($_w); unset($_write[$j]); } } if (empty($_read)) { break; } } $this->getListener()->fire('stop', new Event\Bucket()); return; } protected function getPipe($pipe) { if (!isset($this->_pipes[$pipe])) { throw new Exception( 'Pipe descriptor %d does not exist, cannot read from it.', 2, $pipe ); } return $this->_pipes[$pipe]; } protected function isPipeSeekable($pipe) { if (!isset($this->_seekable[$pipe])) { $_pipe = $this->getPipe($pipe); $data = stream_get_meta_data($_pipe); $this->_seekable[$pipe] = $data['seekable']; } return $this->_seekable[$pipe]; } public function eof($pipe = 1) { return feof($this->getPipe($pipe)); } public function read($length, $pipe = 1) { if (0 > $length) { throw new Exception( 'Length must be greater than 0, given %d.', 3, $length ); } return fread($this->getPipe($pipe), $length); } public function readString($length, $pipe = 1) { return $this->read($length, $pipe); } public function readCharacter($pipe = 1) { return fgetc($this->getPipe($pipe)); } public function readBoolean($pipe = 1) { return (bool) $this->read(1, $pipe); } public function readInteger($length = 1, $pipe = 1) { return (int) $this->read($length, $pipe); } public function readFloat($length = 1, $pipe = 1) { return (float) $this->read($length, $pipe); } public function readArray($format = null, $pipe = 1) { return $this->scanf($format, $pipe); } public function readLine($pipe = 1) { return stream_get_line($this->getPipe($pipe), 1 << 15, "\n"); } public function readAll($offset = -1, $pipe = 1) { $_pipe = $this->getPipe($pipe); if (true === $this->isPipeSeekable($pipe)) { $offset += ftell($_pipe); } else { $offset = -1; } return stream_get_contents($_pipe, -1, $offset); } public function scanf($format, $pipe = 1) { return fscanf($this->getPipe($pipe), $format); } public function write($string, $length, $pipe = 0) { if (0 > $length) { throw new Exception( 'Length must be greater than 0, given %d.', 4, $length ); } return fwrite($this->getPipe($pipe), $string, $length); } public function writeString($string, $pipe = 0) { $string = (string) $string; return $this->write($string, strlen($string), $pipe); } public function writeCharacter($char, $pipe = 0) { return $this->write((string) $char[0], 1, $pipe); } public function writeBoolean($boolean, $pipe = 0) { return $this->write((string) (bool) $boolean, 1, $pipe); } public function writeInteger($integer, $pipe = 0) { $integer = (string) (int) $integer; return $this->write($integer, strlen($integer), $pipe); } public function writeFloat($float, $pipe = 0) { $float = (string) (float) $float; return $this->write($float, strlen($float), $pipe); } public function writeArray(array $array, $pipe = 0) { $array = var_export($array, true); return $this->write($array, strlen($array), $pipe); } public function writeLine($line, $pipe = 0) { if (false === $n = strpos($line, "\n")) { return $this->write($line . "\n", strlen($line) + 1, $pipe); } ++$n; return $this->write(substr($line, 0, $n), $n, $pipe); } public function writeAll($string, $pipe = 0) { return $this->write($string, strlen($string), $pipe); } public function truncate($size, $pipe = 0) { return ftruncate($this->getPipe($pipe), $size); } public function getBasename() { return basename($this->getCommand()); } public function getDirname() { return dirname($this->getCommand()); } public function getStatus() { return proc_get_status($this->getStream()); } public function getExitCode() { $handle = $this->getStatus(); return $handle['exitcode']; } public function isSuccessful() { return 0 === $this->getExitCode(); } public function terminate($signal = self::SIGTERM) { return proc_terminate($this->getStream(), $signal); } protected function setCommand($command) { $old = $this->_command; $this->_command = escapeshellcmd($command); return $old; } public function getCommand() { return $this->_command; } protected function setOptions(array $options) { foreach ($options as &$option) { $option = escapeshellarg($option); } $old = $this->_options; $this->_options = $options; return $old; } public function getOptions() { return $this->_options; } public function getCommandLine() { $out = $this->getCommand(); foreach ($this->getOptions() as $key => $value) { if (!is_int($key)) { $out .= ' ' . $key . '=' . $value; } else { $out .= ' ' . $value; } } return $out; } protected function setCwd($cwd) { $old = $this->_cwd; $this->_cwd = $cwd; return $old; } public function getCwd() { return $this->_cwd; } protected function setEnvironment(array $environment) { $old = $this->_environment; $this->_environment = $environment; return $old; } public function getEnvironment() { return $this->_environment; } public function setTimeout($timeout) { $old = $this->_timeout; $this->_timeout = $timeout; return $old; } public function getTimeout() { return $this->_timeout; } public static function setTitle($title) { if (PHP_VERSION_ID < 50500) { return; } cli_set_process_title($title); return; } public static function getTitle() { if (PHP_VERSION_ID < 50500) { return null; } return cli_get_process_title(); } public static function locate($binary) { if (isset($_ENV['PATH'])) { $separator = ':'; $path = &$_ENV['PATH']; } elseif (isset($_SERVER['PATH'])) { $separator = ':'; $path = &$_SERVER['PATH']; } elseif (isset($_SERVER['Path'])) { $separator = ';'; $path = &$_SERVER['Path']; } else { return null; } foreach (explode($separator, $path) as $directory) { if (true === file_exists($out = $directory . DS . $binary)) { return $out; } } return null; } public static function execute($commandLine, $escape = true) { if (true === $escape) { $commandLine = escapeshellcmd($commandLine); } return rtrim(shell_exec($commandLine)); } } setAutocompleters($autocompleters); return; } public function complete(&$prefix) { foreach ($this->getAutocompleters() as $autocompleter) { $preg = preg_match( '#(' . $autocompleter->getWordDefinition() . ')$#u', $prefix, $match ); if (0 === $preg) { continue; } $_prefix = $match[0]; if (null === $out = $autocompleter->complete($_prefix)) { continue; } $prefix = $_prefix; return $out; } return null; } protected function setAutocompleters(array $autocompleters) { $old = $this->_autocompleters; $this->_autocompleters = new \ArrayObject($autocompleters); return $old; } public function getAutocompleters() { return $this->_autocompleters; } public function getWordDefinition() { return '.*'; } } setRoot($root); if (null !== $iteratorFactory) { $this->setIteratorFactory($iteratorFactory); } return; } public function complete(&$prefix) { $root = $this->getRoot(); if (static::PWD === $root) { $root = getcwd(); } $path = $root . DS . $prefix; if (!is_dir($path)) { $path = dirname($path) . DS; $prefix = basename($prefix); } else { $prefix = null; } $iteratorFactory = $this->getIteratorFactory() ?: static::getDefaultIteratorFactory(); try { $iterator = $iteratorFactory($path); $out = []; $length = mb_strlen($prefix); foreach ($iterator as $fileinfo) { $filename = $fileinfo->getFilename(); if (null === $prefix || (mb_substr($filename, 0, $length) === $prefix)) { if ($fileinfo->isDir()) { $out[] = $filename . '/'; } else { $out[] = $filename; } } } } catch (\Exception $e) { return null; } $count = count($out); if (1 === $count) { return $out[0]; } if (0 === $count) { return null; } return $out; } public function getWordDefinition() { return '/?[\w\d\\_\-\.]+(/[\w\d\\_\-\.]*)*'; } public function setRoot($root) { $old = $this->_root; $this->_root = $root; return $old; } public function getRoot() { return $this->_root; } public function setIteratorFactory(\Closure $iteratorFactory) { $old = $this->_iteratorFactory; $this->_iteratorFactory = $iteratorFactory; return $old; } public function getIteratorFactory() { return $this->_iteratorFactory; } public static function getDefaultIteratorFactory() { return function ($path) { return new \DirectoryIterator($path); }; } } setWords($words); return; } public function complete(&$prefix) { $out = []; $length = mb_strlen($prefix); foreach ($this->getWords() as $word) { if (mb_substr($word, 0, $length) === $prefix) { $out[] = $word; } } if (empty($out)) { return null; } if (1 === count($out)) { return $out[0]; } return $out; } public function getWordDefinition() { return '\b\w+'; } public function setWords(array $words) { $old = $this->_words; $this->_words = $words; return $old; } public function getWords() { return $this->_words; } } _mapping["\033[A"] = xcallable($this, '_bindArrowUp'); $this->_mapping["\033[B"] = xcallable($this, '_bindArrowDown'); $this->_mapping["\033[C"] = xcallable($this, '_bindArrowRight'); $this->_mapping["\033[D"] = xcallable($this, '_bindArrowLeft'); $this->_mapping["\001"] = xcallable($this, '_bindControlA'); $this->_mapping["\002"] = xcallable($this, '_bindControlB'); $this->_mapping["\005"] = xcallable($this, '_bindControlE'); $this->_mapping["\006"] = xcallable($this, '_bindControlF'); $this->_mapping["\010"] = $this->_mapping["\177"] = xcallable($this, '_bindBackspace'); $this->_mapping["\027"] = xcallable($this, '_bindControlW'); $this->_mapping["\n"] = xcallable($this, '_bindNewline'); $this->_mapping["\t"] = xcallable($this, '_bindTab'); return; } public function readLine($prefix = null) { $input = Console::getInput(); if (true === $input->eof()) { return false; } $direct = Console::isDirect($input->getStream()->getStream()); $output = Console::getOutput(); if (false === $direct || OS_WIN) { $out = $input->readLine(); if (false === $out) { return false; } $out = substr($out, 0, -1); if (true === $direct) { $output->writeAll($prefix); } else { $output->writeAll($prefix . $out . "\n"); } return $out; } $this->resetLine(); $this->setPrefix($prefix); $read = [$input->getStream()->getStream()]; $output->writeAll($prefix); while (true) { @stream_select($read, $write, $except, 30, 0); if (empty($read)) { $read = [$input->getStream()->getStream()]; continue; } $char = $this->_read(); $this->_buffer = $char; $return = $this->_readLine($char); if (0 === ($return & self::STATE_NO_ECHO)) { $output->writeAll($this->_buffer); } if (0 !== ($return & self::STATE_BREAK)) { break; } } return $this->getLine(); } public function _readLine($char) { if (isset($this->_mapping[$char]) && is_callable($this->_mapping[$char])) { $mapping = $this->_mapping[$char]; return $mapping($this); } if (isset($this->_mapping[$char])) { $this->_buffer = $this->_mapping[$char]; } elseif (false === Ustring::isCharPrintable($char)) { Console\Cursor::bip(); return static::STATE_CONTINUE | static::STATE_NO_ECHO; } if ($this->getLineLength() == $this->getLineCurrent()) { $this->appendLine($this->_buffer); return static::STATE_CONTINUE; } $this->insertLine($this->_buffer); $tail = mb_substr( $this->getLine(), $this->getLineCurrent() - 1 ); $this->_buffer = "\033[K" . $tail . str_repeat( "\033[D", mb_strlen($tail) - 1 ); return static::STATE_CONTINUE; } public function addMappings(array $mappings) { foreach ($mappings as $key => $mapping) { $this->addMapping($key, $mapping); } return; } public function addMapping($key, $mapping) { if ('\e[' === substr($key, 0, 3)) { $this->_mapping["\033[" . substr($key, 3)] = $mapping; } elseif ('\C-' === substr($key, 0, 3)) { $_key = ord(strtolower(substr($key, 3))) - 96; $this->_mapping[chr($_key)] = $mapping; } else { $this->_mapping[$key] = $mapping; } return; } public function addHistory($line = null) { if (empty($line)) { return; } $this->_history[] = $line; $this->_historyCurrent = $this->_historySize++; return; } public function clearHistory() { unset($this->_history); $this->_history = []; $this->_historyCurrent = 0; $this->_historySize = 1; return; } public function getHistory($i = null) { if (null === $i) { $i = $this->_historyCurrent; } if (!isset($this->_history[$i])) { return null; } return $this->_history[$i]; } public function previousHistory() { if (0 >= $this->_historyCurrent) { return $this->getHistory(0); } return $this->getHistory($this->_historyCurrent--); } public function nextHistory() { if ($this->_historyCurrent + 1 >= $this->_historySize) { return $this->getLine(); } return $this->getHistory(++$this->_historyCurrent); } public function getLine() { return $this->_line; } public function appendLine($append) { $this->_line .= $append; $this->_lineLength = mb_strlen($this->_line); $this->_lineCurrent = $this->_lineLength; return; } public function insertLine($insert) { if ($this->_lineLength == $this->_lineCurrent) { return $this->appendLine($insert); } $this->_line = mb_substr($this->_line, 0, $this->_lineCurrent) . $insert . mb_substr($this->_line, $this->_lineCurrent); $this->_lineLength = mb_strlen($this->_line); $this->_lineCurrent += mb_strlen($insert); return; } protected function resetLine() { $this->_line = null; $this->_lineCurrent = 0; $this->_lineLength = 0; return; } public function getLineCurrent() { return $this->_lineCurrent; } public function getLineLength() { return $this->_lineLength; } public function setPrefix($prefix) { $this->_prefix = $prefix; return; } public function getPrefix() { return $this->_prefix; } public function getBuffer() { return $this->_buffer; } public function setAutocompleter(Autocompleter $autocompleter) { $old = $this->_autocompleter; $this->_autocompleter = $autocompleter; return $old; } public function getAutocompleter() { return $this->_autocompleter; } public function _read($length = 512) { return Console::getInput()->read($length); } public function setLine($line) { $this->_line = $line; $this->_lineLength = mb_strlen($this->_line); $this->_lineCurrent = $this->_lineLength; return; } public function setLineCurrent($current) { $this->_lineCurrent = $current; return; } public function setLineLength($length) { $this->_lineLength = $length; return; } public function setBuffer($buffer) { $this->_buffer = $buffer; return; } public function _bindArrowUp(Readline $self) { if (0 === (static::STATE_CONTINUE & static::STATE_NO_ECHO)) { Console\Cursor::clear('↔'); Console::getOutput()->writeAll($self->getPrefix()); } $self->setBuffer($buffer = $self->previousHistory()); $self->setLine($buffer); return static::STATE_CONTINUE; } public function _bindArrowDown(Readline $self) { if (0 === (static::STATE_CONTINUE & static::STATE_NO_ECHO)) { Console\Cursor::clear('↔'); Console::getOutput()->writeAll($self->getPrefix()); } $self->setBuffer($buffer = $self->nextHistory()); $self->setLine($buffer); return static::STATE_CONTINUE; } public function _bindArrowRight(Readline $self) { if ($self->getLineLength() > $self->getLineCurrent()) { if (0 === (static::STATE_CONTINUE & static::STATE_NO_ECHO)) { Console\Cursor::move('→'); } $self->setLineCurrent($self->getLineCurrent() + 1); } $self->setBuffer(null); return static::STATE_CONTINUE; } public function _bindArrowLeft(Readline $self) { if (0 < $self->getLineCurrent()) { if (0 === (static::STATE_CONTINUE & static::STATE_NO_ECHO)) { Console\Cursor::move('←'); } $self->setLineCurrent($self->getLineCurrent() - 1); } $self->setBuffer(null); return static::STATE_CONTINUE; } public function _bindBackspace(Readline $self) { $buffer = null; if (0 < $self->getLineCurrent()) { if (0 === (static::STATE_CONTINUE & static::STATE_NO_ECHO)) { Console\Cursor::move('←'); Console\Cursor::clear('→'); } if ($self->getLineLength() == $current = $self->getLineCurrent()) { $self->setLine(mb_substr($self->getLine(), 0, -1)); } else { $line = $self->getLine(); $current = $self->getLineCurrent(); $tail = mb_substr($line, $current); $buffer = $tail . str_repeat("\033[D", mb_strlen($tail)); $self->setLine(mb_substr($line, 0, $current - 1) . $tail); $self->setLineCurrent($current - 1); } } $self->setBuffer($buffer); return static::STATE_CONTINUE; } public function _bindControlA(Readline $self) { for ($i = $self->getLineCurrent() - 1; 0 <= $i; --$i) { $self->_bindArrowLeft($self); } return static::STATE_CONTINUE; } public function _bindControlB(Readline $self) { $current = $self->getLineCurrent(); if (0 === $current) { return static::STATE_CONTINUE; } $words = preg_split( '#\b#u', $self->getLine(), -1, PREG_SPLIT_OFFSET_CAPTURE | PREG_SPLIT_NO_EMPTY ); for ( $i = 0, $max = count($words) - 1; $i < $max && $words[$i + 1][1] < $current; ++$i ); for ($j = $words[$i][1] + 1; $current >= $j; ++$j) { $self->_bindArrowLeft($self); } return static::STATE_CONTINUE; } public function _bindControlE(Readline $self) { for ( $i = $self->getLineCurrent(), $max = $self->getLineLength(); $i < $max; ++$i ) { $self->_bindArrowRight($self); } return static::STATE_CONTINUE; } public function _bindControlF(Readline $self) { $current = $self->getLineCurrent(); if ($self->getLineLength() === $current) { return static::STATE_CONTINUE; } $words = preg_split( '#\b#u', $self->getLine(), -1, PREG_SPLIT_OFFSET_CAPTURE | PREG_SPLIT_NO_EMPTY ); for ( $i = 0, $max = count($words) - 1; $i < $max && $words[$i][1] < $current; ++$i ); if (!isset($words[$i + 1])) { $words[$i + 1] = [1 => $self->getLineLength()]; } for ($j = $words[$i + 1][1]; $j > $current; --$j) { $self->_bindArrowRight($self); } return static::STATE_CONTINUE; } public function _bindControlW(Readline $self) { $current = $self->getLineCurrent(); if (0 === $current) { return static::STATE_CONTINUE; } $words = preg_split( '#\b#u', $self->getLine(), -1, PREG_SPLIT_OFFSET_CAPTURE | PREG_SPLIT_NO_EMPTY ); for ( $i = 0, $max = count($words) - 1; $i < $max && $words[$i + 1][1] < $current; ++$i ); for ($j = $words[$i][1] + 1; $current >= $j; ++$j) { $self->_bindBackspace($self); } return static::STATE_CONTINUE; } public function _bindNewline(Readline $self) { $self->addHistory($self->getLine()); return static::STATE_BREAK; } public function _bindTab(Readline $self) { $output = Console::getOutput(); $autocompleter = $self->getAutocompleter(); $state = static::STATE_CONTINUE | static::STATE_NO_ECHO; if (null === $autocompleter) { return $state; } $current = $self->getLineCurrent(); $line = $self->getLine(); if (0 === $current) { return $state; } $matches = preg_match_all( '#' . $autocompleter->getWordDefinition() . '$#u', mb_substr($line, 0, $current), $words ); if (0 === $matches) { return $state; } $word = $words[0][0]; if ('' === trim($word)) { return $state; } $solution = $autocompleter->complete($word); $length = mb_strlen($word); if (null === $solution) { return $state; } if (is_array($solution)) { $_solution = $solution; $count = count($_solution) - 1; $cWidth = 0; $window = Console\Window::getSize(); $wWidth = $window['x']; $cursor = Console\Cursor::getPosition(); array_walk($_solution, function (&$value) use (&$cWidth) { $handle = mb_strlen($value); if ($handle > $cWidth) { $cWidth = $handle; } return; }); array_walk($_solution, function (&$value) use (&$cWidth) { $handle = mb_strlen($value); if ($handle >= $cWidth) { return; } $value .= str_repeat(' ', $cWidth - $handle); return; }); $mColumns = (int) floor($wWidth / ($cWidth + 2)); $mLines = (int) ceil(($count + 1) / $mColumns); --$mColumns; $i = 0; if (0 > $window['y'] - $cursor['y'] - $mLines) { Console\Window::scroll('↑', $mLines); Console\Cursor::move('↑', $mLines); } Console\Cursor::save(); Console\Cursor::hide(); Console\Cursor::move('↓ LEFT'); Console\Cursor::clear('↓'); foreach ($_solution as $j => $s) { $output->writeAll("\033[0m" . $s . "\033[0m"); if ($i++ < $mColumns) { $output->writeAll(' '); } else { $i = 0; if (isset($_solution[$j + 1])) { $output->writeAll("\n"); } } } Console\Cursor::restore(); Console\Cursor::show(); ++$mColumns; $input = Console::getInput(); $read = [$input->getStream()->getStream()]; $mColumn = -1; $mLine = -1; $coord = -1; $unselect = function () use ( &$mColumn, &$mLine, &$coord, &$_solution, &$cWidth, $output ) { Console\Cursor::save(); Console\Cursor::hide(); Console\Cursor::move('↓ LEFT'); Console\Cursor::move('→', $mColumn * ($cWidth + 2)); Console\Cursor::move('↓', $mLine); $output->writeAll("\033[0m" . $_solution[$coord] . "\033[0m"); Console\Cursor::restore(); Console\Cursor::show(); return; }; $select = function () use ( &$mColumn, &$mLine, &$coord, &$_solution, &$cWidth, $output ) { Console\Cursor::save(); Console\Cursor::hide(); Console\Cursor::move('↓ LEFT'); Console\Cursor::move('→', $mColumn * ($cWidth + 2)); Console\Cursor::move('↓', $mLine); $output->writeAll("\033[7m" . $_solution[$coord] . "\033[0m"); Console\Cursor::restore(); Console\Cursor::show(); return; }; $init = function () use ( &$mColumn, &$mLine, &$coord, &$select ) { $mColumn = 0; $mLine = 0; $coord = 0; $select(); return; }; while (true) { @stream_select($read, $write, $except, 30, 0); if (empty($read)) { $read = [$input->getStream()->getStream()]; continue; } switch ($char = $self->_read()) { case "\033[A": if (-1 === $mColumn && -1 === $mLine) { $init(); break; } $unselect(); $coord = max(0, $coord - $mColumns); $mLine = (int) floor($coord / $mColumns); $mColumn = $coord % $mColumns; $select(); break; case "\033[B": if (-1 === $mColumn && -1 === $mLine) { $init(); break; } $unselect(); $coord = min($count, $coord + $mColumns); $mLine = (int) floor($coord / $mColumns); $mColumn = $coord % $mColumns; $select(); break; case "\t": case "\033[C": if (-1 === $mColumn && -1 === $mLine) { $init(); break; } $unselect(); $coord = min($count, $coord + 1); $mLine = (int) floor($coord / $mColumns); $mColumn = $coord % $mColumns; $select(); break; case "\033[D": if (-1 === $mColumn && -1 === $mLine) { $init(); break; } $unselect(); $coord = max(0, $coord - 1); $mLine = (int) floor($coord / $mColumns); $mColumn = $coord % $mColumns; $select(); break; case "\n": if (-1 !== $mColumn && -1 !== $mLine) { $tail = mb_substr($line, $current); $current -= $length; $self->setLine( mb_substr($line, 0, $current) . $solution[$coord] . $tail ); $self->setLineCurrent( $current + mb_strlen($solution[$coord]) ); Console\Cursor::move('←', $length); $output->writeAll($solution[$coord]); Console\Cursor::clear('→'); $output->writeAll($tail); Console\Cursor::move('←', mb_strlen($tail)); } default: $mColumn = -1; $mLine = -1; $coord = -1; Console\Cursor::save(); Console\Cursor::move('↓ LEFT'); Console\Cursor::clear('↓'); Console\Cursor::restore(); if ("\033" !== $char && "\n" !== $char) { $self->setBuffer($char); return $self->_readLine($char); } break 2; } } return $state; } $tail = mb_substr($line, $current); $current -= $length; $self->setLine( mb_substr($line, 0, $current) . $solution . $tail ); $self->setLineCurrent( $current + mb_strlen($solution) ); Console\Cursor::move('←', $length); $output->writeAll($solution); Console\Cursor::clear('→'); $output->writeAll($tail); Console\Cursor::move('←', mb_strlen($tail)); return $state; } } Console::advancedInteraction(); Consistency::flexEntity('Hoa\Console\Readline\Readline'); _case_get_mode_xxx(0010000, SUT::IS_FIFO); } public function case_get_mode_character() { return $this->_case_get_mode_xxx(0020000, SUT::IS_CHARACTER); } public function case_get_mode_directory() { return $this->_case_get_mode_xxx(0040000, SUT::IS_DIRECTORY); } public function case_get_mode_block() { return $this->_case_get_mode_xxx(0060000, SUT::IS_BLOCK); } public function case_get_mode_regular() { return $this->_case_get_mode_xxx(0100000, SUT::IS_REGULAR); } public function case_get_mode_link() { return $this->_case_get_mode_xxx(0120000, SUT::IS_LINK); } public function case_get_mode_socket() { return $this->_case_get_mode_xxx(0140000, SUT::IS_SOCKET); } public function case_get_mode_whiteout() { return $this->_case_get_mode_xxx(0160000, SUT::IS_WHITEOUT); } public function case_get_mode_unknown() { return $this->_case_get_mode_xxx(0170000, -1); } protected function _case_get_mode_xxx($mask, $expect) { $this ->given($this->function->fstat = ['mode' => $mask & 0170000]) ->when($result = SUT::getMode(null)) ->then ->integer($result) ->isEqualTo($expect); } public function case_set_input() { $this ->given($input = new LUT\Input()) ->when($result = SUT::setInput($input)) ->then ->variable($result) ->isNull() ->object(SUT::getInput()) ->isIdenticalTo($input); } public function case_get_input() { $this ->when($result = SUT::getInput()) ->then ->object($result) ->isInstanceOf('Hoa\Console\Input') ->isIdenticalTo(SUT::getInput()); } public function case_set_output() { $this ->given($output = new LUT\Output()) ->when($result = SUT::setOutput($output)) ->then ->variable($result) ->isNull() ->object(SUT::getOutput()) ->isIdenticalTo($output); } public function case_get_output() { $this ->when($result = SUT::getOutput()) ->then ->object($result) ->isInstanceOf('Hoa\Console\Output') ->isIdenticalTo(SUT::getOutput()); } public function case_set_tput() { $this ->given($tput = new LUT\Tput('hoa://Library/Console/Terminfo/78/xterm')) ->when($result = SUT::setTput($tput)) ->then ->variable($result) ->isNull() ->object(SUT::getTput()) ->isIdenticalTo($tput); } public function case_get_tput() { $this ->when($result = SUT::getTput()) ->then ->object($result) ->isInstanceOf('Hoa\Console\Tput') ->isIdenticalTo(SUT::getTput()); } public function case_is_tmux_running() { $this ->given($_SERVER['TMUX'] = 'foo') ->when($result = SUT::isTmuxRunning()) ->then ->boolean($result) ->isTrue(); } public function case_is_not_tmux_running() { unset($_SERVER['TMUX']); $this ->when($result = SUT::isTmuxRunning()) ->then ->boolean($result) ->isFalse(); } } when(SUT::move('u')) ->then ->output ->isEqualTo("\033[1A"); } public function case_move_up() { $this ->when(SUT::move('up')) ->then ->output ->isEqualTo("\033[1A"); } public function case_move_↑() { $this ->when(SUT::move('↑')) ->then ->output ->isEqualTo("\033[1A"); } public function case_move_↑_repeated() { $this ->when(SUT::move('↑', 42)) ->then ->output ->isEqualTo("\033[42A"); } public function case_move_r() { $this ->when(SUT::move('r')) ->then ->output ->isEqualTo("\033[1C"); } public function case_move_right() { $this ->when(SUT::move('right')) ->then ->output ->isEqualTo("\033[1C"); } public function case_move_→() { $this ->when(SUT::move('→')) ->then ->output ->isEqualTo("\033[1C"); } public function case_move_→_repeated() { $this ->when(SUT::move('→', 42)) ->then ->output ->isEqualTo("\033[42C"); } public function case_move_d() { $this ->when(SUT::move('d')) ->then ->output ->isEqualTo("\033[1B"); } public function case_move_down() { $this ->when(SUT::move('down')) ->then ->output ->isEqualTo("\033[1B"); } public function case_move_↓() { $this ->when(SUT::move('↓')) ->then ->output ->isEqualTo("\033[1B"); } public function case_move_↓_repeated() { $this ->when(SUT::move('↓', 42)) ->then ->output ->isEqualTo("\033[42B"); } public function case_move_l() { $this ->when(SUT::move('l')) ->then ->output ->isEqualTo("\033[1D"); } public function case_move_left() { $this ->when(SUT::move('left')) ->then ->output ->isEqualTo("\033[1D"); } public function case_move_←() { $this ->when(SUT::move('←')) ->then ->output ->isEqualTo("\033[1D"); } public function case_move_←_repeated() { $this ->when(SUT::move('←', 42)) ->then ->output ->isEqualTo("\033[42D"); } public function case_move_sequence() { $this ->when(SUT::move('↑ → ↓ ←')) ->then ->output ->isEqualTo("\033[1A\033[1C\033[1B\033[1D"); } public function case_move_to_x_y() { $this ->when(SUT::moveTo(7, 42)) ->then ->output ->isEqualTo("\033[42;7H"); } public function case_move_to_x() { $this ->given( $file = new File\ReadWrite('hoa://Test/Vfs/Input?type=file'), $file->writeAll("\033[42;7R"), $file->rewind(), $input = LUT::setInput(new LUT\Input($file)) ) ->when(SUT::moveTo(153)) ->then ->output ->isEqualTo("\033[6n\033[42;153H"); } public function case_move_to_y() { $this ->given( $file = new File\ReadWrite('hoa://Test/Vfs/Input?type=file'), $file->writeAll("\033[42;7R"), $file->rewind(), $input = LUT::setInput(new LUT\Input($file)) ) ->when(SUT::moveTo(null, 153)) ->then ->output ->isEqualTo("\033[6n\033[153;7H"); } public function case_get_position() { $this ->given( $file = new File\ReadWrite('hoa://Test/Vfs/Input?type=file'), $file->writeAll("\033[42;7R"), $file->rewind(), $input = LUT::setInput(new LUT\Input($file)) ) ->when($result = SUT::getPosition()) ->then ->output ->isEqualTo("\033[6n") ->array($result) ->isEqualTo([ 'x' => 7, 'y' => 42 ]); } public function case_save() { $this ->when(SUT::save()) ->then ->output ->isEqualTo("\0337"); } public function case_restore() { $this ->when(SUT::restore()) ->then ->output ->isEqualTo("\0338"); } public function case_clear_a() { $this ->when(SUT::clear('a')) ->then ->output ->isEqualTo("\033[H\033[2J\033[1;1H"); } public function case_clear_all() { $this ->when(SUT::clear('all')) ->then ->output ->isEqualTo("\033[H\033[2J\033[1;1H"); } public function case_clear_↕() { $this ->when(SUT::clear('↕')) ->then ->output ->isEqualTo("\033[H\033[2J\033[1;1H"); } public function case_clear_u() { $this ->when(SUT::clear('u')) ->then ->output ->isEqualTo("\033[1J"); } public function case_clear_up() { $this ->when(SUT::clear('up')) ->then ->output ->isEqualTo("\033[1J"); } public function case_clear_↑() { $this ->when(SUT::clear('↑')) ->then ->output ->isEqualTo("\033[1J"); } public function case_clear_r() { $this ->when(SUT::clear('r')) ->then ->output ->isEqualTo("\033[K"); } public function case_clear_right() { $this ->when(SUT::clear('right')) ->then ->output ->isEqualTo("\033[K"); } public function case_clear_→() { $this ->when(SUT::clear('→')) ->then ->output ->isEqualTo("\033[K"); } public function case_clear_d() { $this ->when(SUT::clear('d')) ->then ->output ->isEqualTo("\033[J"); } public function case_clear_down() { $this ->when(SUT::clear('down')) ->then ->output ->isEqualTo("\033[J"); } public function case_clear_↓() { $this ->when(SUT::clear('↓')) ->then ->output ->isEqualTo("\033[J"); } public function case_clear_l() { $this ->when(SUT::clear('l')) ->then ->output ->isEqualTo("\033[1K"); } public function case_clear_left() { $this ->when(SUT::clear('left')) ->then ->output ->isEqualTo("\033[1K"); } public function case_clear_←() { $this ->when(SUT::clear('←')) ->then ->output ->isEqualTo("\033[1K"); } public function case_clear_line() { $this ->when(SUT::clear('line')) ->then ->output ->isEqualTo("\r\033[K"); } public function case_clear_↔() { $this ->when(SUT::clear('↔')) ->then ->output ->isEqualTo("\r\033[K"); } public function case_hide() { $this ->when(SUT::hide()) ->then ->output ->isEqualTo("\033[?25l"); } public function case_show() { $this ->when(SUT::show()) ->then ->output ->isEqualTo("\033[?12;25h"); } public function case_colorize_n() { $this ->when(SUT::colorize('n')) ->then ->output ->isEqualTo("\033[0m"); } public function case_colorize_normal() { $this ->when(SUT::colorize('normal')) ->then ->output ->isEqualTo("\033[0m"); } public function case_colorize_normal_repeated() { $this ->when(SUT::colorize('n normal')) ->then ->output ->isEqualTo("\033[0;0m"); } public function case_colorize_b() { $this ->when(SUT::colorize('b')) ->then ->output ->isEqualTo("\033[1m"); } public function case_colorize_bold() { $this ->when(SUT::colorize('bold')) ->then ->output ->isEqualTo("\033[1m"); } public function case_colorize_u() { $this ->when(SUT::colorize('u')) ->then ->output ->isEqualTo("\033[4m"); } public function case_colorize_underlined() { $this ->when(SUT::colorize('underlined')) ->then ->output ->isEqualTo("\033[4m"); } public function case_colorize_bl() { $this ->when(SUT::colorize('bl')) ->then ->output ->isEqualTo("\033[5m"); } public function case_colorize_blink() { $this ->when(SUT::colorize('blink')) ->then ->output ->isEqualTo("\033[5m"); } public function case_colorize_i() { $this ->when(SUT::colorize('i')) ->then ->output ->isEqualTo("\033[7m"); } public function case_colorize_inverse() { $this ->when(SUT::colorize('inverse')) ->then ->output ->isEqualTo("\033[7m"); } public function case_colorize_not_b() { $this ->when(SUT::colorize('!b')) ->then ->output ->isEqualTo("\033[22m"); } public function case_colorize_not_bold() { $this ->when(SUT::colorize('!bold')) ->then ->output ->isEqualTo("\033[22m"); } public function case_colorize_not_u() { $this ->when(SUT::colorize('!u')) ->then ->output ->isEqualTo("\033[24m"); } public function case_colorize_not_underlined() { $this ->when(SUT::colorize('!underlined')) ->then ->output ->isEqualTo("\033[24m"); } public function case_colorize_not_bl() { $this ->when(SUT::colorize('!bl')) ->then ->output ->isEqualTo("\033[25m"); } public function case_colorize_not_blink() { $this ->when(SUT::colorize('!blink')) ->then ->output ->isEqualTo("\033[25m"); } public function case_colorize_not_i() { $this ->when(SUT::colorize('!i')) ->then ->output ->isEqualTo("\033[27m"); } public function case_colorize_not_inverse() { $this ->when(SUT::colorize('!inverse')) ->then ->output ->isEqualTo("\033[27m"); } public function case_colorize_fg_black() { $this ->when(SUT::colorize('fg(black)')) ->then ->output ->isEqualTo("\033[30m"); } public function case_colorize_foreground_black() { $this ->when(SUT::colorize('foreground(black)')) ->then ->output ->isEqualTo("\033[30m"); } public function case_colorize_fg_red() { $this ->when(SUT::colorize('fg(red)')) ->then ->output ->isEqualTo("\033[31m"); } public function case_colorize_fg_green() { $this ->when(SUT::colorize('fg(green)')) ->then ->output ->isEqualTo("\033[32m"); } public function case_colorize_fg_yellow() { $this ->when(SUT::colorize('fg(yellow)')) ->then ->output ->isEqualTo("\033[33m"); } public function case_colorize_fg_blue() { $this ->when(SUT::colorize('fg(blue)')) ->then ->output ->isEqualTo("\033[34m"); } public function case_colorize_fg_magenta() { $this ->when(SUT::colorize('fg(magenta)')) ->then ->output ->isEqualTo("\033[35m"); } public function case_colorize_fg_cyan() { $this ->when(SUT::colorize('fg(cyan)')) ->then ->output ->isEqualTo("\033[36m"); } public function case_colorize_fg_white() { $this ->when(SUT::colorize('fg(white)')) ->then ->output ->isEqualTo("\033[37m"); } public function case_colorize_fg_default() { $this ->when(SUT::colorize('fg(default)')) ->then ->output ->isEqualTo("\033[39m"); } public function case_colorize_bg_black() { $this ->when(SUT::colorize('bg(black)')) ->then ->output ->isEqualTo("\033[40m"); } public function case_colorize_background_black() { $this ->when(SUT::colorize('background(black)')) ->then ->output ->isEqualTo("\033[40m"); } public function case_colorize_bg_red() { $this ->when(SUT::colorize('bg(red)')) ->then ->output ->isEqualTo("\033[41m"); } public function case_colorize_bg_green() { $this ->when(SUT::colorize('bg(green)')) ->then ->output ->isEqualTo("\033[42m"); } public function case_colorize_bg_yellow() { $this ->when(SUT::colorize('bg(yellow)')) ->then ->output ->isEqualTo("\033[43m"); } public function case_colorize_bg_blue() { $this ->when(SUT::colorize('bg(blue)')) ->then ->output ->isEqualTo("\033[44m"); } public function case_colorize_bg_magenta() { $this ->when(SUT::colorize('bg(magenta)')) ->then ->output ->isEqualTo("\033[45m"); } public function case_colorize_bg_cyan() { $this ->when(SUT::colorize('bg(cyan)')) ->then ->output ->isEqualTo("\033[46m"); } public function case_colorize_bg_white() { $this ->when(SUT::colorize('bg(white)')) ->then ->output ->isEqualTo("\033[47m"); } public function case_colorize_bg_default() { $this ->when(SUT::colorize('bg(default)')) ->then ->output ->isEqualTo("\033[49m"); } public function case_colorize_foreground_ff0066() { $this ->when(SUT::colorize('foreground(#ff0066)')) ->then ->output ->isEqualTo("\033[38;5;197m"); } public function case_colorize_background_ff0066() { $this ->when(SUT::colorize('background(#ff0066)')) ->then ->output ->isEqualTo("\033[48;5;197m"); } public function case_colorize_foreground_color_index() { $this ->when(SUT::colorize('foreground(42)')) ->then ->output ->isEqualTo("\033[38;5;42m"); } public function case_change_color() { $this ->when(SUT::changeColor(35, 0xff0066)) ->then ->output ->isEqualTo("\033]4;35;ff0066\033\\"); } public function case_set_style_b() { $this ->given($this->constant->OS_WIN = false) ->when(SUT::setStyle('b')) ->then ->output ->isEqualTo("\033[1 q"); } public function case_set_style_block() { $this ->given($this->constant->OS_WIN = false) ->when(SUT::setStyle('block')) ->then ->output ->isEqualTo("\033[1 q"); } public function case_set_style_▋() { $this ->given($this->constant->OS_WIN = false) ->when(SUT::setStyle('▋')) ->then ->output ->isEqualTo("\033[1 q"); } public function case_set_style_block_no_blink() { $this ->given($this->constant->OS_WIN = false) ->when(SUT::setStyle('block', false)) ->then ->output ->isEqualTo("\033[2 q"); } public function case_set_style_u() { $this ->given($this->constant->OS_WIN = false) ->when(SUT::setStyle('u')) ->then ->output ->isEqualTo("\033[2 q"); } public function case_set_style_underline() { $this ->given($this->constant->OS_WIN = false) ->when(SUT::setStyle('underline')) ->then ->output ->isEqualTo("\033[2 q"); } public function case_set_style__() { $this ->given($this->constant->OS_WIN = false) ->when(SUT::setStyle('_')) ->then ->output ->isEqualTo("\033[2 q"); } public function case_set_style_underline_no_blink() { $this ->given($this->constant->OS_WIN = false) ->when(SUT::setStyle('underline', false)) ->then ->output ->isEqualTo("\033[3 q"); } public function case_set_style_v() { $this ->given($this->constant->OS_WIN = false) ->when(SUT::setStyle('v')) ->then ->output ->isEqualTo("\033[5 q"); } public function case_set_style_vertical() { $this ->given($this->constant->OS_WIN = false) ->when(SUT::setStyle('vertical')) ->then ->output ->isEqualTo("\033[5 q"); } public function case_set_style_pipe() { $this ->given($this->constant->OS_WIN = false) ->when(SUT::setStyle('|')) ->then ->output ->isEqualTo("\033[5 q"); } public function case_set_style_vertical_no_blink() { $this ->given($this->constant->OS_WIN = false) ->when(SUT::setStyle('vertical', false)) ->then ->output ->isEqualTo("\033[6 q"); } public function case_set_style_on_windows() { $this ->given($this->constant->OS_WIN = true) ->when(SUT::setStyle('b')) ->then ->output ->isEmpty(); } public function case_bip() { $this ->when(SUT::bip()) ->then ->output ->isEqualTo("\007"); } } given( $parser = new LUT\Parser(), $parser->parse(''), $options = new SUT([], $parser) ) ->when($result = $options->getOption($value)) ->then ->boolean($options->isPipetteEmpty()) ->isTrue() ->boolean($result) ->isFalse() ->variable($value) ->isNull(); } public function case_one_entry() { $this ->given( $parser = new LUT\Parser(), $parser->parse('--foo'), $options = new SUT( [ ['foo', SUT::NO_ARGUMENT, 'f'] ], $parser ) ) ->when($result = $options->getOption($value)) ->then ->boolean($options->isPipetteEmpty()) ->isFalse() ->string($result) ->isEqualTo('f') ->boolean($value) ->isTrue() ->when($result = $options->getOption($value)) ->boolean($options->isPipetteEmpty()) ->isFalse() ->boolean($result) ->isFalse() ->variable($value) ->isNull(); } public function case_more_entries() { $this ->given( $parser = new LUT\Parser(), $parser->parse('--foo --bar baz'), $options = new SUT( [ ['foo', SUT::NO_ARGUMENT, 'f'], ['bar', SUT::REQUIRED_ARGUMENT, 'b'] ], $parser ) ) ->when($result = $options->getOption($value)) ->then ->boolean($options->isPipetteEmpty()) ->isFalse() ->string($result) ->isEqualTo('f') ->boolean($value) ->isTrue() ->when($result = $options->getOption($value)) ->then ->boolean($options->isPipetteEmpty()) ->isFalse() ->string($result) ->isEqualTo('b') ->string($value) ->isEqualTo('baz') ->when($result = $options->getOption($value)) ->then ->boolean($options->isPipetteEmpty()) ->isFalse() ->boolean($result) ->isFalse() ->variable($value) ->isNull(); } public function case_ambiguous() { $this ->given( $parser = new LUT\Parser(), $parser->parse('--baz'), $options = new SUT( [ ['foo', SUT::NO_ARGUMENT, 'f'], ['bar', SUT::REQUIRED_ARGUMENT, 'b'] ], $parser ) ) ->when($result = $options->getOption($value)) ->then ->boolean($options->isPipetteEmpty()) ->isFalse() ->string($result) ->isEqualTo('__ambiguous') ->array($value) ->isEqualTo([ 'solutions' => ['bar'], 'value' => true, 'option' => 'baz' ]); } public function case_resolve_option_ambiguity_no_solution() { $this ->given( $parser = new LUT\Parser(), $parser->parse(''), $options = new SUT([], $parser), $solutions = [ 'solutions' => [], 'value' => true, 'option' => 'baz' ] ) ->exception(function () use ($options, $solutions) { $options->resolveOptionAmbiguity($solutions); }) ->isInstanceOf('Hoa\Console\Exception'); } public function case_resolve_option_ambiguity() { $this ->given( $parser = new LUT\Parser(), $parser->parse('--baz'), $options = new SUT( [ ['bar', SUT::NO_ARGUMENT, 'b'] ], $parser ) ) ->when($result = $options->getOption($value)) ->then ->string($result) ->isEqualTo('__ambiguous') ->array($value) ->isEqualTo([ 'solutions' => ['bar'], 'value' => true, 'option' => 'baz' ]) ->when($result = $options->resolveOptionAmbiguity($value)) ->then ->variable($result) ->isNull() ->when($result = $options->getOption($value)) ->then ->string($result) ->isEqualTo('b') ->boolean($value) ->isEqualTo(true); } } when($result = new SUT()) ->then ->object($result) ->isInstanceOf('Hoa\Stream\IStream\In'); } public function case_eof() { $this ->given( $file = new File\ReadWrite('hoa://Test/Vfs/Input?type=file'), $input = new SUT($file) ) ->when($result = $input->eof()) ->then ->boolean($result) ->isEqualTo($file->eof()); } public function case_read() { $this ->given( $file = new File\ReadWrite('hoa://Test/Vfs/Input?type=file'), $file->writeAll('foobar'), $file->rewind(), $input = new SUT($file) ) ->when($result = $input->read(3)) ->then ->string($result) ->isEqualTo('foo'); } public function case_read_string() { $this ->given( $file = new File\ReadWrite('hoa://Test/Vfs/Input?type=file'), $file->writeAll('foobar'), $file->rewind(), $input = new SUT($file) ) ->when($result = $input->readString(3)) ->then ->string($result) ->isEqualTo('foo'); } public function case_read_character() { $this ->given( $file = new File\ReadWrite('hoa://Test/Vfs/Input?type=file'), $file->writeAll('foobar'), $file->rewind(), $input = new SUT($file) ) ->when($result = $input->readCharacter(1)) ->then ->string($result) ->isEqualTo('f'); } public function case_read_boolean_true() { $this ->given( $file = new File\ReadWrite('hoa://Test/Vfs/Input?type=file'), $file->writeAll('1'), $file->rewind(), $input = new SUT($file) ) ->when($result = $input->readBoolean()) ->then ->boolean($result) ->isTrue(); } public function case_read_boolean_false() { $this ->given( $file = new File\ReadWrite('hoa://Test/Vfs/Input?type=file'), $file->writeAll('0'), $file->rewind(), $input = new SUT($file) ) ->when($result = $input->readBoolean()) ->then ->boolean($result) ->isFalse(); } public function case_read_integer() { $this ->given( $file = new File\ReadWrite('hoa://Test/Vfs/Input?type=file'), $file->writeAll('42'), $file->rewind(), $input = new SUT($file) ) ->when($result = $input->readInteger(2)) ->then ->integer($result) ->isEqualTo(42); } public function case_read_float() { $this ->given( $file = new File\ReadWrite('hoa://Test/Vfs/Input?type=file'), $file->writeAll('4.2'), $file->rewind(), $input = new SUT($file) ) ->when($result = $input->readFloat(3)) ->then ->float($result) ->isEqualTo(4.2); } public function case_read_array() { $this ->given( $file = new File\ReadWrite('hoa://Test/Vfs/Input?type=file'), $file->writeAll('foo bar'), $file->rewind(), $input = new SUT($file) ) ->when($result = $input->readArray('%s %s')) ->then ->array($result) ->isEqualTo([ 0 => 'foo', 1 => 'bar' ]); } public function case_read_line() { $this ->given( $file = new File\ReadWrite('hoa://Test/Vfs/Input?type=file'), $file->writeAll('foo' . "\n" . 'bar'), $file->rewind(), $input = new SUT($file) ) ->when($result = $input->readLine()) ->then ->string($result) ->isEqualTo('foo' . "\n"); } public function case_read_all() { $this ->given( $content = '4.2foo' . "\n" . 'bar', $file = new File\ReadWrite('hoa://Test/Vfs/Input?type=file'), $file->writeAll($content), $file->rewind(), $input = new SUT($file) ) ->when($result = $input->readAll()) ->then ->string($result) ->isEqualTo($content); } public function case_scanf() { $this ->given( $file = new File\ReadWrite('hoa://Test/Vfs/Input?type=file'), $file->writeAll('foo 42' . "\n" . 'bar 153'), $file->rewind(), $input = new SUT($file) ) ->when($result = $input->scanf('%s %d')) ->then ->array($result) ->isEqualTo([ 0 => 'foo', 1 => 42 ]) ->when($result = $input->scanf('%s %d')) ->then ->array($result) ->isEqualTo([ 0 => 'bar', 1 => 153 ]); } } when($result = SUT::getInstance()) ->then ->object($result) ->isIdenticalTo(SUT::getInstance()); } public function case_track_button_left() { return $this->_case_track( 7, 42, SUT::BUTTON_LEFT, 'mousedown', [ 'x' => 7, 'y' => 42, 'button' => 'left', 'shift' => false, 'meta' => false, 'ctrl' => false ] ); } public function case_track_button_middle() { return $this->_case_track( 7, 42, SUT::BUTTON_MIDDLE, 'mousedown', [ 'x' => 7, 'y' => 42, 'button' => 'middle', 'shift' => false, 'meta' => false, 'ctrl' => false ] ); } public function case_track_button_right() { return $this->_case_track( 7, 42, SUT::BUTTON_RIGHT, 'mousedown', [ 'x' => 7, 'y' => 42, 'button' => 'right', 'shift' => false, 'meta' => false, 'ctrl' => false ] ); } public function case_track_button_release() { return $this->_case_track( 7, 42, SUT::BUTTON_RELEASE, 'mouseup', [ 'x' => 7, 'y' => 42, 'button' => null, 'shift' => false, 'meta' => false, 'ctrl' => false ] ); } public function case_track_wheelup() { return $this->_case_track( 7, 42, SUT::WHEEL_UP, 'wheelup', [ 'x' => 7, 'y' => 42, 'button' => null, 'shift' => false, 'meta' => false, 'ctrl' => false ] ); } public function case_track_wheeldown() { return $this->_case_track( 7, 42, SUT::WHEEL_DOWN, 'wheeldown', [ 'x' => 7, 'y' => 42, 'button' => null, 'shift' => false, 'meta' => false, 'ctrl' => false ] ); } public function _case_track($x, $y, $pointerActionCode, $listenerName, array $listenerData) { $this ->given( $self = $this, $file = new File\ReadWrite('hoa://Test/Vfs/Input?type=file'), $file->writeAll( "\033[M" . chr(($pointerActionCode + 32) & ~28) . chr($x + 32) . chr($y + 32) ), $file->rewind(), $input = LUT::setInput(new LUT\Input($file)), $this->function->stream_select = function () { static $i = 1; if (1 === $i) { return $i--; } return false; }, SUT::getInstance()->on( $listenerName, function (Event\Bucket $bucket) use (&$_listenerData) { $_listenerData = $bucket->getData(); return; } ) ) ->when(SUT::track()) ->then ->output ->isEqualTo( "\033[1;2'z" . "\033[?1000h" . "\033[?1003h" . "\033[?1003l" . "\033[?1000l" ) ->array($_listenerData) ->isEqualTo($listenerData); } public function case_untrack_when_not_tracked() { $this ->when($result = SUT::untrack()) ->then ->variable($result) ->isNull() ->output ->isEmpty(); } } when($result = new SUT()) ->then ->object($result) ->isInstanceOf('Hoa\Stream\IStream\Out'); } public function case_write() { $this ->given($output = new SUT()) ->when($output->write('foobar', 3)) ->then ->output ->isIdenticalTo('foo'); } public function case_write_string() { $this ->given($output = new SUT()) ->when($output->writeString(123)) ->then ->output ->isIdenticalTo('123'); } public function case_write_character() { $this ->given($output = new SUT()) ->when($output->writeCharacter('foo')) ->then ->output ->isIdenticalTo('f'); } public function case_write_boolean_true() { $this ->given($output = new SUT()) ->when($output->writeBoolean(true)) ->then ->output ->isIdenticalTo('1'); } public function case_write_boolean_false() { $this ->given($output = new SUT()) ->when($output->writeBoolean(false)) ->then ->output ->isIdenticalTo('0'); } public function case_write_integer() { $this ->given($output = new SUT()) ->when($output->writeInteger(-42)) ->then ->output ->isIdenticalTo('-42'); } public function case_write_float() { $this ->given($output = new SUT()) ->when($output->writeFloat(-4.2)) ->then ->output ->isIdenticalTo('-4.2'); } public function case_write_array() { $this ->given($output = new SUT()) ->when($output->writeArray(['foo' => 'bar'])) ->then ->output ->isIdenticalTo( 'array (' . "\n" . ' \'foo\' => \'bar\',' . "\n" . ')' ); } public function case_write_line_no_newline() { $this ->given($output = new SUT()) ->when($output->writeLine('foo')) ->then ->output ->isIdenticalTo('foo' . "\n"); } public function case_write_line_with_newline() { $this ->given($output = new SUT()) ->when($output->writeLine('foo' . "\n")) ->then ->output ->isIdenticalTo('foo' . "\n"); } public function case_write_line_with_newlines() { $this ->given($output = new SUT()) ->when($output->writeLine('foo' . "\n" . 'bar' . "\n")) ->then ->output ->isIdenticalTo('foo' . "\n"); } public function case_write_all() { $this ->given($output = new SUT()) ->when($output->writeAll('foobar')) ->then ->output ->isIdenticalTo('foobar'); } public function case_truncate() { $this ->given($output = new SUT()) ->when($result = $output->truncate(42)) ->then ->boolean($result) ->isFalse(); } public function case_default_multiplexer_consideration() { $this ->given($output = new SUT()) ->when($result = $output->isMultiplexerConsidered()) ->then ->boolean($result) ->isFalse(); } public function case_consider_multiplexer() { $this ->given($output = new SUT()) ->when( $output->considerMultiplexer(true), $result = $output->isMultiplexerConsidered() ) ->then ->boolean($result) ->isTrue(); } } _case( '-a -b -c', ['a' => true, 'b' => true, 'c' => true] ); } public function case_single_dashed_short_options() { return $this->_case( '-abc', ['a' => true, 'b' => true, 'c' => true] ); } public function case_long_options() { return $this->_case( '--foo --bar --b-a-z', ['foo' => true, 'bar' => true, 'b-a-z' => true] ); } public function case_boolean_switches() { return $this->_case( '-a -a --foo --foo --bar --bar --bar', ['a' => false, 'foo' => false, 'bar' => true] ); } public function case_valued_switches_equal_simple() { return $this->_case( '-a=foo --long=bar', ['a' => 'foo', 'long' => 'bar'] ); } public function case_valued_switches_equal_with_escaped_space() { return $this->_case( '-a=fo\ o --long=b\ a\ r', ['a' => 'fo o', 'long' => 'b a r'] ); } public function case_valued_switches_equal_double_quoted() { return $this->_case( '-a="fo\"o" --long="b\"a\'r"', ['a' => 'fo"o', 'long' => 'b"a\'r'] ); } public function case_valued_switches_equal_single_quoted() { return $this->_case( '-a=\'fo\\\'"o\' --long=\'b\\\'a"r\'', ['a' => 'fo\'"o', 'long' => 'b\'a"r'] ); } public function case_valued_switches_space_simple() { return $this->_case( '-a foo --long bar', ['a' => 'foo', 'long' => 'bar'] ); } public function case_valued_switches_space_with_escaped_space() { return $this->_case( '-a fo\ o --long b\ a\ r', ['a' => 'fo o', 'long' => 'b a r'] ); } public function case_valued_switches_space_double_quoted() { return $this->_case( '-a "fo\"o" --long "b\"a\'r"', ['a' => 'fo"o', 'long' => 'b"a\'r'] ); } public function case_valued_switches_space_single_quoted() { return $this->_case( '-a \'fo\\\'"o\' --long \'b\\\'a"r\'', ['a' => 'fo\'"o', 'long' => 'b\'a"r'] ); } public function case_valued_switch_equal_negative_value() { return $this->_case( '-a=-foo --long=-bar', ['a' => '-foo', 'long' => '-bar'] ); } public function case_special_valued_switch() { return $this->_case( '-a f,o,o --long b,a,r', ['a' => 'f,o,o', 'long' => 'b,a,r'] ); } public function case_input_associated_to_a_short_option() { return $this->_case( '-a input', ['a' => 'input'] ); } public function case_double_dashes_input() { return $this->_case( '-a --long bar -- inputA inputB', ['a' => true, 'long' => 'bar'], ['inputA', 'inputB'] ); } public function case_simple_input() { return $this->_case( 'inputA inputB', [], ['inputA', 'inputB'] ); } public function case_valued_switch_followed_by_an_input() { return $this->_case( '-a foo --long bar inputA inputB', ['a' => 'foo', 'long' => 'bar'], ['inputA', 'inputB'] ); } public function case_unordered() { return $this->_case( 'inputA -a foo inputB --long bar inputC', ['a' => 'foo', 'long' => 'bar'], ['inputA', 'inputB', 'inputC'] ); } protected function _case($command, array $switches, array $inputs = []) { $this ->given($parser = new SUT()) ->when($result = $parser->parse($command)) ->then ->variable($result) ->isNull() ->array($parser->getSwitches()) ->isIdenticalTo($switches) ->array($parser->getInputs()) ->isIdenticalTo($inputs); } public function case_state_is_reset() { $this ->given($parser = new SUT()) ->when($result = $parser->parse('--foo=bar baz')) ->then ->variable($result) ->isNull() ->array($parser->getSwitches()) ->isIdenticalTo(['foo' => 'bar']) ->array($parser->getInputs()) ->isIdenticalTo(['baz']) ->when($result = $parser->parse('--bar=baz qux')) ->variable($result) ->isNull() ->array($parser->getSwitches()) ->isIdenticalTo(['bar' => 'baz']) ->array($parser->getInputs()) ->isIdenticalTo(['qux']); } public function case_parse_special_value_list() { $this ->given($parser = new SUT()) ->when($result = $parser->parseSpecialValue('foo,bar,baz')) ->then ->array($result) ->isIdenticalTo([ 'foo', 'bar', 'baz' ]); } public function case_parse_special_value_list_with_keywords() { $this ->given($parser = new SUT()) ->when($result = $parser->parseSpecialValue('foo,bar,QUX', ['QUX' => 'baz'])) ->then ->array($result) ->isIdenticalTo([ 'foo', 'bar', 'baz' ]); } public function case_parse_special_value_list_with_range() { $this ->given($parser = new SUT()) ->when($result = $parser->parseSpecialValue('foo,bar,1:3')) ->then ->array($result) ->isIdenticalTo([ 1, 2, 3, 'foo', 'bar' ]); } public function case_set_long_only() { $this ->given($parser = new SUT()) ->when( $parser->setLongOnly(true), $result = $parser->parse('-abc') ) ->then ->boolean($parser->getLongOnly()) ->isTrue() ->variable($result) ->isNull() ->array($parser->getSwitches()) ->isIdenticalTo([ 'abc' => true, ]) ->array($parser->getInputs()) ->isEmpty(); } } given($autocompleter = new SUT([])) ->when($result = $autocompleter->getWordDefinition()) ->then ->string($result) ->isEqualTo('.*'); } public function case_constructor() { $this ->given( $autocompleterA = new \Mock\Hoa\Console\Readline\Autocompleter\Autocompleter(), $autocompleterB = new \Mock\Hoa\Console\Readline\Autocompleter\Autocompleter() ) ->when($result = new SUT([$autocompleterA, $autocompleterB])) ->then ->object($result) ->isInstanceOf('Hoa\Console\Readline\Autocompleter\Autocompleter') ->let($autocompleters = $result->getAutocompleters()) ->object($autocompleters) ->isInstanceOf('ArrayObject') ->integer(count($autocompleters)) ->isEqualTo(2) ->object($autocompleters[0]) ->isIdenticalTo($autocompleterA) ->object($autocompleters[1]) ->isIdenticalTo($autocompleterB); } public function case_complete_no_solution() { $this ->given( $autocompleterA = new \Mock\Hoa\Console\Readline\Autocompleter\Autocompleter(), $autocompleterA->getWordDefinition = function () { return 'aaa'; }, $autocompleterB = new \Mock\Hoa\Console\Readline\Autocompleter\Autocompleter(), $autocompleterB->getWordDefinition = function () { return 'bbb'; }, $autocompleter = new SUT([$autocompleterA, $autocompleterB]), $prefix = 'ccc' ) ->when($result = $autocompleter->complete($prefix)) ->then ->variable($result) ->isNull() ->string($prefix) ->isEqualTo('ccc'); } public function case_complete_one_solution_first_autocompleter() { $self = $this; $this ->given( $autocompleterA = new \Mock\Hoa\Console\Readline\Autocompleter\Autocompleter(), $this->calling($autocompleterA)->getWordDefinition = function () { return 'aaa'; }, $this->calling($autocompleterA)->complete = function ($prefix) use ($self) { $self ->string($prefix) ->isEqualTo('aaa'); return 'AAA'; }, $autocompleterB = new \Mock\Hoa\Console\Readline\Autocompleter\Autocompleter(), $this->calling($autocompleterB)->getWordDefinition = function () { return 'bbb'; }, $this->calling($autocompleterB)->complete = function ($prefix) use ($self) { $self->fail('Bad autocompleter called.'); }, $autocompleter = new SUT([$autocompleterA, $autocompleterB]), $prefix = 'aaa' ) ->when($result = $autocompleter->complete($prefix)) ->then ->string($result) ->isEqualTo('AAA') ->string($prefix) ->isEqualTo('aaa'); } public function case_complete_one_solution_second_autocompleter() { $self = $this; $this ->given( $autocompleterA = new \Mock\Hoa\Console\Readline\Autocompleter\Autocompleter(), $this->calling($autocompleterA)->getWordDefinition = function () { return 'aaa'; }, $this->calling($autocompleterA)->complete = function ($prefix) use ($self) { $self->fail('Bad autocompleter called.'); }, $autocompleterB = new \Mock\Hoa\Console\Readline\Autocompleter\Autocompleter(), $this->calling($autocompleterB)->getWordDefinition = function () { return 'bbb'; }, $this->calling($autocompleterB)->complete = function ($prefix) use ($self) { $self ->string($prefix) ->isEqualTo('bbb'); return 'BBB'; }, $autocompleter = new SUT([$autocompleterA, $autocompleterB]), $prefix = 'bbb' ) ->when($result = $autocompleter->complete($prefix)) ->then ->string($result) ->isEqualTo('BBB') ->string($prefix) ->isEqualTo('bbb'); } } given($autocompleter = new SUT()) ->when($result = $autocompleter->getWordDefinition()) ->then ->string($result) ->isEqualTo('/?[\w\d\\_\-\.]+(/[\w\d\\_\-\.]*)*'); } public function case_constructor() { $this ->given( $root = 'foo', $iteratorFactory = function () { return 42; } ) ->when($result = new SUT($root, $iteratorFactory)) ->then ->object($result) ->isInstanceOf('Hoa\Console\Readline\Autocompleter\Autocompleter') ->string($result->getRoot()) ->isEqualTo($root) ->object($result->getIteratorFactory()) ->isIdenticalTo($iteratorFactory); } public function case_complete_no_solution() { $this ->given( resolve('hoa://Test/Vfs/Root?type=directory'), resolve('hoa://Test/Vfs/Root/Foo?type=file'), resolve('hoa://Test/Vfs/Root/Bar?type=file'), $autocompleter = new SUT('hoa://Test/Vfs/Root'), $prefix = 'Q' ) ->when($result = $autocompleter->complete($prefix)) ->then ->variable($result) ->isNull() ->string($prefix) ->isEqualTo('Q'); } public function case_complete_one_solution() { $this ->given( resolve('hoa://Test/Vfs/Root?type=directory'), resolve('hoa://Test/Vfs/Root/Foo?type=file'), resolve('hoa://Test/Vfs/Root/Bar?type=file'), $autocompleter = new SUT('hoa://Test/Vfs/Root'), $prefix = 'F' ) ->when($result = $autocompleter->complete($prefix)) ->then ->string($result) ->isEqualTo('Foo') ->string($prefix) ->isEqualTo('F'); } public function case_complete_with_smallest_prefix() { $this ->given( resolve('hoa://Test/Vfs/Root?type=directory'), resolve('hoa://Test/Vfs/Root/Foo?type=file'), resolve('hoa://Test/Vfs/Root/Bar?type=file'), resolve('hoa://Test/Vfs/Root/Baz?type=file'), resolve('hoa://Test/Vfs/Root/Qux?type=file'), $autocompleter = new SUT('hoa://Test/Vfs/Root'), $prefix = 'B' ) ->when($result = $autocompleter->complete($prefix)) ->then ->array($result) ->isEqualTo(['Bar', 'Baz']) ->string($prefix) ->isEqualTo('B'); } public function case_complete_with_longer_prefix() { $this ->given( resolve('hoa://Test/Vfs/Root?type=directory'), resolve('hoa://Test/Vfs/Root/Bara?type=file'), resolve('hoa://Test/Vfs/Root/Barb?type=file'), resolve('hoa://Test/Vfs/Root/Baza?type=file'), $autocompleter = new SUT('hoa://Test/Vfs/Root'), $prefix = 'Bar' ) ->when($result = $autocompleter->complete($prefix)) ->then ->array($result) ->isEqualTo(['Bara', 'Barb']) ->string($prefix) ->isEqualTo('Bar'); } public function case_set_root() { $this ->given($autocompleter = new SUT()) ->when($result = $autocompleter->setRoot('foo')) ->then ->variable($result) ->isNull() ->when($result = $autocompleter->setRoot('bar')) ->then ->string($result) ->isEqualTo('foo'); } public function case_get_root() { $this ->given( $autocompleter = new SUT(), $autocompleter->setRoot('foo') ) ->when($result = $autocompleter->getRoot()) ->then ->string($result) ->isEqualTo('foo'); } public function case_set_iterator_factory() { $this ->given($autocompleter = new SUT()) ->when( $result = $autocompleter->setIteratorFactory(function () { return 42; }) ) ->then ->variable($result) ->isNull() ->when( $result = $autocompleter->setIteratorFactory(function () { return 43; }) ) ->then ->integer($result()) ->isEqualTo(42); } public function case_get_iterator_factory() { $this ->given( $autocompleter = new SUT(), $autocompleter->setIteratorFactory(function () { return 42; }) ) ->when(function () use (&$result, $autocompleter) { $result = $autocompleter->getIteratorFactory(); }) ->then ->integer($result()) ->isEqualTo(42); } public function case_get_default_iterator_factory() { $this ->when(function () use (&$result) { $result = SUT::getDefaultIteratorFactory(); }) ->then ->object($result) ->isInstanceOf('Closure') ->object($result(__DIR__)) ->isInstanceOf('DirectoryIterator'); } } given($words = ['foo', 'bar', 'baz', 'qux']) ->when($result = new SUT($words)) ->then ->object($result) ->isInstanceOf('Hoa\Console\Readline\Autocompleter\Autocompleter') ->array($result->getWords()) ->isEqualTo($words); } public function case_complete_no_solution() { $this ->given( $autocompleter = new SUT(['foo', 'bar']), $prefix = 'q' ) ->when($result = $autocompleter->complete($prefix)) ->then ->variable($result) ->isNull() ->string($prefix) ->isEqualTo('q'); } public function case_complete_one_solution() { $this ->given( $autocompleter = new SUT(['foo', 'bar']), $prefix = 'f' ) ->when($result = $autocompleter->complete($prefix)) ->then ->string($result) ->isEqualTo('foo') ->string($prefix) ->isEqualTo('f'); } public function case_complete_with_smallest_prefix() { $this ->given( $autocompleter = new SUT(['foo', 'bar', 'baz', 'qux']), $prefix = 'b' ) ->when($result = $autocompleter->complete($prefix)) ->then ->array($result) ->isEqualTo(['bar', 'baz']) ->string($prefix) ->isEqualTo('b'); } public function case_complete_with_longer_prefix() { $this ->given( $autocompleter = new SUT(['bara', 'barb', 'baza']), $prefix = 'bar' ) ->when($result = $autocompleter->complete($prefix)) ->then ->array($result) ->isEqualTo(['bara', 'barb']) ->string($prefix) ->isEqualTo('bar'); } public function case_get_word_definition() { $this ->given($autocompleter = new SUT([])) ->when($result = $autocompleter->getWordDefinition()) ->then ->string($result) ->isEqualTo('\b\w+'); } public function case_set_words() { $this ->given( $words = ['foo', 'bar', 'baz', 'qux'], $autocompleter = new SUT([]) ) ->when($result = $autocompleter->setWords($words)) ->then ->array($result) ->isEmpty(); } public function case_get_words() { $this ->given( $words = ['foo', 'bar', 'baz', 'qux'], $autocompleter = new SUT($words) ) ->when($result = $autocompleter->getWords()) ->then ->array($result) ->isEqualTo($words); } } when($result = SUT::STATE_CONTINUE) ->then ->integer($result) ->isEqualTo( LUT\Readline::STATE_CONTINUE | LUT\Readline::STATE_NO_ECHO ); } } given($_SERVER['TERM'] = 'foo') ->when($result = SUT::getTerm()) ->then ->string($result) ->isEqualTo('foo'); } public function case_get_unknown_term_on_windows() { unset($_SERVER['TERM']); $this ->given($this->constant->OS_WIN = true) ->when($result = SUT::getTerm()) ->then ->string($result) ->isEqualTo('windows-ansi'); } public function case_get_unknown_term() { unset($_SERVER['TERM']); $this ->given($this->constant->OS_WIN = false) ->when($result = SUT::getTerm()) ->then ->string($result) ->isEqualTo('xterm'); } public function case_unknown_file_when_parsing() { $this ->exception(function () { new SUT('/hoa/flatland'); }) ->isInstanceOf('Hoa\Console\Exception'); } public function case_all_informations() { $this ->given($tput = new SUT('hoa://Library/Console/Terminfo/78/xterm')) ->when($result = $tput->getInformations()) ->then ->array($result) ->isIdenticalTo([ 'file' => 'hoa://Library/Console/Terminfo/78/xterm', 'headers' => [ 'data_size' => 3258, 'header_size' => 12, 'magic_number' => 282, 'names_size' => 48, 'bool_count' => 38, 'number_count' => 15, 'string_count' => 413, 'string_table_size' => 1388 ], 'name' => 'xterm', 'description' => 'xterm terminal emulator (X Window System)', 'booleans' => [ 'auto_left_margin' => false, 'auto_right_margin' => true, 'no_esc_ctlc' => false, 'ceol_standout_glitch' => false, 'eat_newline_glitch' => true, 'erase_overstrike' => false, 'generic_type' => false, 'hard_copy' => false, 'meta_key' => true, 'status_line' => false, 'insert_null_glitch' => false, 'memory_above' => false, 'memory_below' => false, 'move_insert_mode' => true, 'move_standout_mode' => true, 'over_strike' => false, 'status_line_esc_ok' => false, 'dest_tabs_magic_smso' => false, 'tilde_glitch' => false, 'transparent_underline' => false, 'xon_xoff' => false, 'needs_xon_xoff' => false, 'prtr_silent' => true, 'hard_cursor' => false, 'non_rev_rmcup' => false, 'no_pad_char' => true, 'non_dest_scroll_region' => false, 'can_change' => false, 'back_color_erase' => true, 'hue_lightness_saturation' => false, 'col_addr_glitch' => false, 'cr_cancels_micro_mode' => false, 'print_wheel' => false, 'row_addr_glitch' => false, 'semi_auto_right_margin' => false, 'cpi_changes_res' => false, 'lpi_changes_res' => false, 'backspaces_with_bs' => true ], 'numbers' => [ 'columns' => 80, 'init_tabs' => 8, 'lines' => 24, 'lines_of_memory' => -1, 'magic_cookie_glitch' => -1, 'padding_baud_rate' => -1, 'virtual_terminal' => -1, 'width_status_line' => -1, 'num_labels' => -1, 'label_height' => -1, 'label_width' => -1, 'max_attributes' => -1, 'maximum_windows' => -1, 'max_colors' => 8, 'max_pairs' => 64 ], 'strings' => [ 'back_tab' => '', 'bell' => '', 'carriage_return' => ' ', 'change_scroll_region' => '[%i%p1%d;%p2%dr', 'clear_all_tabs' => '', 'clear_screen' => '', 'clr_eol' => '', 'clr_eos' => '', 'column_address' => '[%i%p1%dG', 'cursor_address' => '[%i%p1%d;%p2%dH', 'cursor_down' => "\n", 'cursor_home' => '', 'cursor_invisible' => '[?25l', 'cursor_left' => '', 'cursor_normal' => '[?12l[?25h', 'cursor_right' => '', 'cursor_up' => '', 'cursor_visible' => '[?12;25h', 'delete_character' => '', 'delete_line' => '', 'enter_alt_charset_mode' => '(0', 'enter_blink_mode' => '', 'enter_bold_mode' => '', 'enter_ca_mode' => '[?1049h', 'enter_insert_mode' => '', 'enter_secure_mode' => '', 'enter_reverse_mode' => '', 'enter_standout_mode' => '', 'enter_underline_mode' => '', 'erase_chars' => '[%p1%dX', 'exit_alt_charset_mode' => '(B', 'exit_attribute_mode' => '(B', 'exit_ca_mode' => '[?1049l', 'exit_insert_mode' => '', 'exit_standout_mode' => '', 'exit_underline_mode' => '', 'flash_screen' => '[?5h$<100/>[?5l', 'init_2string' => '[!p[?3;4l>', 'insert_line' => '', 'key_backspace' => '', 'key_dc' => '[3~', 'key_down' => 'OB', 'key_f1' => 'OP', 'key_f10' => '[21~', 'key_f2' => 'OQ', 'key_f3' => 'OR', 'key_f4' => 'OS', 'key_f5' => '[15~', 'key_f6' => '[17~', 'key_f7' => '[18~', 'key_f8' => '[19~', 'key_f9' => '[20~', 'key_home' => 'OH', 'key_ic' => '[2~', 'key_left' => 'OD', 'key_npage' => '[6~', 'key_ppage' => '[5~', 'key_right' => 'OC', 'key_sf' => '', 'key_sr' => '', 'key_up' => 'OA', 'keypad_local' => '[?1l>', 'keypad_xmit' => '[?1h=', 'meta_off' => '[?1034l', 'meta_on' => '[?1034h', 'parm_dch' => '[%p1%dP', 'parm_delete_line' => '[%p1%dM', 'parm_down_cursor' => '[%p1%dB', 'parm_ich' => '[%p1%d@', 'parm_index' => '[%p1%dS', 'parm_insert_line' => '[%p1%dL', 'parm_left_cursor' => '[%p1%dD', 'parm_right_cursor' => '[%p1%dC', 'parm_rindex' => '[%p1%dT', 'parm_up_cursor' => '[%p1%dA', 'print_screen' => '', 'prtr_off' => '', 'prtr_on' => '', 'reset_1string' => 'c', 'reset_2string' => '[!p[?3;4l>', 'restore_cursor' => '8', 'row_address' => '[%i%p1%dd', 'save_cursor' => '7', 'scroll_forward' => "\n", 'scroll_reverse' => 'M', 'set_attributes' => '%?%p9%t(0%e(B%;[0%?%p6%t;1%;%?%p2%t;4%;%?%p1%p3%|%t;7%;%?%p4%t;5%;%?%p7%t;8%;m', 'set_tab' => 'H', 'tab' => ' ', 'key_b2' => 'OE', 'acs_chars' => '``aaffggiijjkkllmmnnooppqqrrssttuuvvwwxxyyzz{{||}}~~', 'key_btab' => '', 'enter_am_mode' => '[?7h', 'exit_am_mode' => '[?7l', 'key_end' => 'OF', 'key_enter' => 'OM', 'key_sdc' => '[3;2~', 'key_send' => '', 'key_shome' => '', 'key_sic' => '[2;2~', 'key_sleft' => '', 'key_snext' => '[6;2~', 'key_sprevious' => '[5;2~', 'key_sright' => '', 'key_f11' => '[23~', 'key_f12' => '[24~', 'key_f13' => '', 'key_f14' => '', 'key_f15' => '', 'key_f16' => '', 'key_f17' => '[15;2~', 'key_f18' => '[17;2~', 'key_f19' => '[18;2~', 'key_f20' => '[19;2~', 'key_f21' => '[20;2~', 'key_f22' => '[21;2~', 'key_f23' => '[23;2~', 'key_f24' => '[24;2~', 'key_f25' => '', 'key_f26' => '', 'key_f27' => '', 'key_f28' => '', 'key_f29' => '[15;5~', 'key_f30' => '[17;5~', 'key_f31' => '[18;5~', 'key_f32' => '[19;5~', 'key_f33' => '[20;5~', 'key_f34' => '[21;5~', 'key_f35' => '[23;5~', 'key_f36' => '[24;5~', 'key_f37' => '', 'key_f38' => '', 'key_f39' => '', 'key_f40' => '', 'key_f41' => '[15;6~', 'key_f42' => '[17;6~', 'key_f43' => '[18;6~', 'key_f44' => '[19;6~', 'key_f45' => '[20;6~', 'key_f46' => '[21;6~', 'key_f47' => '[23;6~', 'key_f48' => '[24;6~', 'key_f49' => '', 'key_f50' => '', 'key_f51' => '', 'key_f52' => '', 'key_f53' => '[15;3~', 'key_f54' => '[17;3~', 'key_f55' => '[18;3~', 'key_f56' => '[19;3~', 'key_f57' => '[20;3~', 'key_f58' => '[21;3~', 'key_f59' => '[23;3~', 'key_f60' => '[24;3~', 'key_f61' => '', 'key_f62' => '', 'key_f63' => '', 'clr_bol' => '', 'user6' => '[%i%d;%dR', 'user7' => '', 'user8' => '[?1;2c', 'user9' => '', 'orig_pair' => '', 'set_foreground' => '[3%?%p1%{1}%=%t4%e%p1%{3}%=%t6%e%p1%{4}%=%t1%e%p1%{6}%=%t3%e%p1%d%;m', 'set_background' => '[4%?%p1%{1}%=%t4%e%p1%{3}%=%t6%e%p1%{4}%=%t1%e%p1%{6}%=%t3%e%p1%d%;m', 'key_mouse' => '', 'set_a_foreground' => '[3%p1%dm', 'set_a_background' => '[4%p1%dm', 'memory_lock' => 'l', 'memory_unlock' => 'm' ] ]); } public function case_has() { $this ->given($tput = new SUT('hoa://Library/Console/Terminfo/78/xterm')) ->when($result = $tput->has('auto_left_margin')) ->then ->boolean($result) ->isFalse() ->when($result = $tput->has('auto_right_margin')) ->then ->boolean($result) ->isTrue(); } public function case_has_unknown_boolean() { $this ->given($tput = new SUT('hoa://Library/Console/Terminfo/78/xterm')) ->when($result = $tput->has('💩')) ->then ->boolean($result) ->isFalse(); } public function case_count() { $this ->given($tput = new SUT('hoa://Library/Console/Terminfo/78/xterm')) ->when($result = $tput->count('columns')) ->then ->integer($result) ->isEqualTo(80); } public function case_count_unknown_integer() { $this ->given($tput = new SUT('hoa://Library/Console/Terminfo/78/xterm')) ->when($result = $tput->count('💩')) ->then ->integer($result) ->isEqualTo(0); } public function case_get() { $this ->given($tput = new SUT('hoa://Library/Console/Terminfo/78/xterm')) ->when($result = $tput->get('cursor_down')) ->then ->string($result) ->isEqualTo("\n"); } public function case_get_unknown_string() { $this ->given($tput = new SUT('hoa://Library/Console/Terminfo/78/xterm')) ->when($result = $tput->get('💩')) ->then ->variable($result) ->isNull(); } } when($result = SUT::getInstance()) ->then ->object($result) ->isIdenticalTo(SUT::getInstance()); } public function case_set_size() { $this ->given($this->constant->OS_WIN = false) ->when(SUT::setSize(7, 42)) ->then ->output ->isEqualTo("\033[8;42;7t"); } public function case_set_size_on_windows() { $this ->given($this->constant->OS_WIN = true) ->when(SUT::setSize(7, 42)) ->then ->output ->isEmpty(); } public function case_move_to() { $this ->given($this->constant->OS_WIN = false) ->when(SUT::moveTo(7, 42)) ->then ->output ->isEqualTo("\033[3;7;42t"); } public function case_move_to_on_windows() { $this ->given($this->constant->OS_WIN = true) ->when(SUT::moveTo(7, 42)) ->then ->output ->isEmpty(); } public function case_get_position() { $this ->given( $this->constant->OS_WIN = false, $file = new File\ReadWrite('hoa://Test/Vfs/Input?type=file'), $file->writeAll("\033[3;7;42t"), $file->rewind(), $input = LUT::setInput(new LUT\Input($file)) ) ->when($result = SUT::getPosition()) ->then ->output ->isEqualTo("\033[13t") ->array($result) ->isEqualTo([ 'x' => 7, 'y' => 42 ]); } public function case_get_position_on_windows() { $this ->given($this->constant->OS_WIN = true) ->when($result = SUT::getPosition()) ->then ->variable($result) ->isNull() ->output ->isEmpty(); } public function case_scroll_u() { $this ->given($this->constant->OS_WIN = false) ->when(SUT::scroll('u')) ->then ->output ->isEqualTo("\033[1S"); } public function case_scroll_up() { $this ->given($this->constant->OS_WIN = false) ->when(SUT::scroll('up')) ->then ->output ->isEqualTo("\033[1S"); } public function case_scroll_d() { $this ->given($this->constant->OS_WIN = false) ->when(SUT::scroll('d')) ->then ->output ->isEqualTo("\033[1T"); } public function case_scroll_down() { $this ->given($this->constant->OS_WIN = false) ->when(SUT::scroll('d')) ->then ->output ->isEqualTo("\033[1T"); } public function case_scroll_u_d_up_down() { $this ->given($this->constant->OS_WIN = false) ->when(SUT::scroll('u d up down')) ->then ->output ->isEqualTo("\033[2S\033[2T"); } public function case_scroll_up_repeated() { $this ->given($this->constant->OS_WIN = false) ->when(SUT::scroll('up', 3)) ->then ->output ->isEqualTo("\033[3S"); } public function case_scroll_on_windows() { $this ->given($this->constant->OS_WIN = true) ->when(SUT::scroll('u')) ->then ->output ->isEmpty(); } public function case_minimize() { $this ->given($this->constant->OS_WIN = false) ->when(SUT::minimize()) ->then ->output ->isEqualTo("\033[2t"); } public function case_minimize_on_windows() { $this ->given($this->constant->OS_WIN = true) ->when(SUT::minimize()) ->then ->output ->isEmpty(); } public function case_restore() { $this ->given($this->constant->OS_WIN = false) ->when(SUT::restore()) ->then ->output ->isEqualTo("\033[1t"); } public function case_restore_on_windows() { $this ->given($this->constant->OS_WIN = true) ->when(SUT::restore()) ->then ->output ->isEmpty(); } public function case_raise() { $this ->given($this->constant->OS_WIN = false) ->when(SUT::raise()) ->then ->output ->isEqualTo("\033[5t"); } public function case_raise_on_windows() { $this ->given($this->constant->OS_WIN = true) ->when(SUT::raise()) ->then ->output ->isEmpty(); } public function case_lower() { $this ->given($this->constant->OS_WIN = false) ->when(SUT::lower()) ->then ->output ->isEqualTo("\033[6t"); } public function case_lower_on_windows() { $this ->given($this->constant->OS_WIN = true) ->when(SUT::lower()) ->then ->output ->isEmpty(); } public function case_set_title() { $this ->given($this->constant->OS_WIN = false) ->when(SUT::setTitle('foobar 😄')) ->then ->output ->isEqualTo("\033]0;foobar 😄\033\\"); } public function case_set_title_on_windows() { $this ->given($this->constant->OS_WIN = true) ->when(SUT::setTitle('foobar 😄')) ->then ->output ->isEmpty(); } public function case_get_title() { $this ->given( $this->constant->OS_WIN = false, $title = 'hello 🌍', $file = new File\ReadWrite('hoa://Test/Vfs/Input?type=file'), $file->writeAll("\033]l" . $title . "\033\\"), $file->rewind(), $input = LUT::setInput(new LUT\Input($file)), $this->function->stream_select = function () { return 1; } ) ->when($result = SUT::getTitle()) ->then ->output ->isEqualTo("\033[21t") ->string($result) ->isEqualTo($title); } public function case_get_title_on_windows() { $this ->given($this->constant->OS_WIN = true) ->when($result = SUT::getTitle()) ->then ->variable($result) ->isNull() ->output ->isEmpty(); } public function case_get_title_timed_out() { $this ->given( $this->function->stream_select = function () { return 0; } ) ->when($result = SUT::getTitle()) ->then ->output ->isEqualTo("\033[21t") ->variable($result) ->isNull(); } public function case_get_label() { $this ->given( $this->constant->OS_WIN = false, $label = 'hello 🌍', $file = new File\ReadWrite('hoa://Test/Vfs/Input?type=file'), $file->writeAll("\033]L" . $label . "\033\\"), $file->rewind(), $input = LUT::setInput(new LUT\Input($file)), $this->function->stream_select = function () { return 1; } ) ->when($result = SUT::getLabel()) ->then ->output ->isEqualTo("\033[20t") ->string($result) ->isEqualTo($label); } public function case_get_label_timed_out() { $this ->given( $this->function->stream_select = function () { return 0; } ) ->when($result = SUT::getLabel()) ->then ->output ->isEqualTo("\033[20t") ->variable($result) ->isNull(); } public function case_get_label_on_windows() { $this ->given($this->constant->OS_WIN = true) ->when($result = SUT::getLabel()) ->then ->variable($result) ->isNull() ->output ->isEmpty(); } public function case_refresh() { $this ->given($this->constant->OS_WIN = false) ->when(SUT::refresh()) ->then ->output ->isEqualTo("\033[7t"); } public function case_refresh_on_windows() { $this ->given($this->constant->OS_WIN = true) ->when(SUT::refresh()) ->then ->output ->isEmpty(); } public function case_copy() { unset($_SERVER['TMUX']); $this ->given($this->constant->OS_WIN = false) ->when(SUT::copy('bla')) ->then ->output ->isEqualTo("\033]52;;" . base64_encode('bla') . "\033\\"); } public function case_copy_on_tmux() { $this ->given( $_SERVER['TMUX'] = 'foo', $this->constant->OS_WIN = false ) ->when(SUT::copy('bla')) ->then ->output ->isEqualTo( "\033Ptmux;" . "\033\033]52;;" . base64_encode('bla') . "\033\033\\" . "\033\\" ); } public function case_copy_on_windows() { $this ->given($this->constant->OS_WIN = true) ->when(SUT::copy('bla')) ->then ->output ->isEmpty(); } } parse($terminfo); return; } protected function parse($terminfo) { if (!file_exists($terminfo)) { throw new Exception( 'Terminfo file %s does not exist.', 0, $terminfo ); } $data = file_get_contents($terminfo); $length = strlen($data); $out = ['file' => $terminfo]; $headers = [ 'data_size' => $length, 'header_size' => 12, 'magic_number' => (ord($data[ 1]) << 8) | ord($data[ 0]), 'names_size' => (ord($data[ 3]) << 8) | ord($data[ 2]), 'bool_count' => (ord($data[ 5]) << 8) | ord($data[ 4]), 'number_count' => (ord($data[ 7]) << 8) | ord($data[ 6]), 'string_count' => (ord($data[ 9]) << 8) | ord($data[ 8]), 'string_table_size' => (ord($data[11]) << 8) | ord($data[10]), ]; $out['headers'] = $headers; $i = $headers['header_size']; $nameAndDescription = explode('|', substr($data, $i, $headers['names_size'] - 1)); $out['name'] = $nameAndDescription[0]; $out['description'] = $nameAndDescription[1]; $i += $headers['names_size']; $booleans = []; $booleanNames = &static::$_booleans; for ( $e = 0, $max = $i + $headers['bool_count']; $i < $max; ++$e, ++$i ) { $booleans[$booleanNames[$e]] = 1 === ord($data[$i]); } $out['booleans'] = $booleans; if (1 === ($i % 2)) { ++$i; } $numbers = []; $numberNames = &static::$_numbers; for ( $e = 0, $max = $i + $headers['number_count'] * 2; $i < $max; ++$e, $i += 2 ) { $name = $numberNames[$e]; $data_i0 = ord($data[$i ]); $data_i1 = ord($data[$i + 1]); if ($data_i1 === 255 && $data_i0 === 255) { $numbers[$name] = -1; } else { $numbers[$name] = ($data_i1 << 8) | $data_i0; } } $out['numbers'] = $numbers; $strings = []; $stringNames = &static::$_strings; $ii = $i + $headers['string_count'] * 2; for ( $e = 0, $max = $ii; $i < $max; ++$e, $i += 2 ) { $name = $stringNames[$e]; $data_i0 = ord($data[$i ]); $data_i1 = ord($data[$i + 1]); if ($data_i1 === 255 && $data_i0 === 255) { continue; } $a = ($data_i1 << 8) | $data_i0; $strings[$name] = $a; if (65534 === $a) { continue; } $b = $ii + $a; $c = $b; while ($c < $length && ord($data[$c])) { $c++; } $value = substr($data, $b, $c - $b); $strings[$name] = false !== $value ? $value : null; } $out['strings'] = $strings; return $this->_informations = $out; } public function getInformations() { return $this->_informations; } public function has($boolean) { if (!isset($this->_informations['booleans'][$boolean])) { return false; } return $this->_informations['booleans'][$boolean]; } public function count($number) { if (!isset($this->_informations['numbers'][$number])) { return 0; } return $this->_informations['numbers'][$number]; } public function get($string) { if (!isset($this->_informations['strings'][$string])) { return null; } return $this->_informations['strings'][$string]; } public static function getTerm() { return isset($_SERVER['TERM']) && !empty($_SERVER['TERM']) ? $_SERVER['TERM'] : (OS_WIN ? 'windows-ansi' : 'xterm'); } public static function getTerminfo($term = null) { $paths = []; if (isset($_SERVER['TERMINFO'])) { $paths[] = $_SERVER['TERMINFO']; } if (isset($_SERVER['HOME'])) { $paths[] = $_SERVER['HOME'] . DS . '.terminfo'; } if (isset($_SERVER['TERMINFO_DIRS'])) { foreach (explode(':', $_SERVER['TERMINFO_DIRS']) as $path) { $paths[] = $path; } } $paths[] = '/usr/share/terminfo'; $paths[] = '/usr/share/lib/terminfo'; $paths[] = '/lib/terminfo'; $paths[] = '/usr/lib/terminfo'; $paths[] = '/usr/local/share/terminfo'; $paths[] = '/usr/local/share/lib/terminfo'; $paths[] = '/usr/local/lib/terminfo'; $paths[] = '/usr/local/ncurses/lib/terminfo'; $paths[] = 'hoa://Library/Console/Terminfo'; $term = $term ?: static::getTerm(); $fileHexa = dechex(ord($term[0])) . DS . $term; $fileAlpha = $term[0] . DS . $term; $pathname = null; foreach ($paths as $path) { if (file_exists($_ = $path . DS . $fileHexa) || file_exists($_ = $path . DS . $fileAlpha)) { $pathname = $_; break; } } if (null === $pathname && 'xterm' !== $term) { return static::getTerminfo('xterm'); } return $pathname; } } writeAll("\033[8;" . $y . ";" . $x . "t"); return; } public static function getSize() { if (OS_WIN) { $modecon = explode("\n", ltrim(Processus::execute('mode con'))); $_y = trim($modecon[2]); preg_match('#[^:]+:\s*([0-9]+)#', $_y, $matches); $y = (int) $matches[1]; $_x = trim($modecon[3]); preg_match('#[^:]+:\s*([0-9]+)#', $_x, $matches); $x = (int) $matches[1]; return [ 'x' => $x, 'y' => $y ]; } $term = ''; if (isset($_SERVER['TERM'])) { $term = 'TERM="' . $_SERVER['TERM'] . '" '; } $command = $term . 'tput cols && ' . $term . 'tput lines'; $tput = Processus::execute($command, false); if (!empty($tput)) { list($x, $y) = explode("\n", $tput); return [ 'x' => intval($x), 'y' => intval($y) ]; } Console::getOutput()->writeAll("\033[18t"); $input = Console::getInput(); $input->read(4); $x = null; $y = null; $handle = &$y; do { $char = $input->readCharacter(); switch ($char) { case ';': $handle = &$x; break; case 't': break 2; default: if (false === ctype_digit($char)) { break 2; } $handle .= $char; } } while (true); if (null === $x || null === $y) { return [ 'x' => 0, 'y' => 0 ]; } return [ 'x' => (int) $x, 'y' => (int) $y ]; } public static function moveTo($x, $y) { if (OS_WIN) { return; } Console::getOutput()->writeAll("\033[3;" . $x . ";" . $y . "t"); return; } public static function getPosition() { if (OS_WIN) { return; } Console::getOutput()->writeAll("\033[13t"); $input = Console::getInput(); $input->read(4); $x = null; $y = null; $handle = &$x; do { $char = $input->readCharacter(); switch ($char) { case ';': $handle = &$y; break; case 't': break 2; default: $handle .= $char; } } while (true); return [ 'x' => (int) $x, 'y' => (int) $y ]; } public static function scroll($directions, $repeat = 1) { if (OS_WIN) { return; } if (1 > $repeat) { return; } elseif (1 === $repeat) { $handle = explode(' ', $directions); } else { $handle = explode(' ', $directions, 1); } $tput = Console::getTput(); $count = ['up' => 0, 'down' => 0]; foreach ($handle as $direction) { switch ($direction) { case 'u': case 'up': case '↑': ++$count['up']; break; case 'd': case 'down': case '↓': ++$count['down']; break; } } $output = Console::getOutput(); if (0 < $count['up']) { $output->writeAll( str_replace( '%p1%d', $count['up'] * $repeat, $tput->get('parm_index') ) ); } if (0 < $count['down']) { $output->writeAll( str_replace( '%p1%d', $count['down'] * $repeat, $tput->get('parm_rindex') ) ); } return; } public static function minimize() { if (OS_WIN) { return; } Console::getOutput()->writeAll("\033[2t"); return; } public static function restore() { if (OS_WIN) { return; } Console::getOutput()->writeAll("\033[1t"); return; } public static function raise() { if (OS_WIN) { return; } Console::getOutput()->writeAll("\033[5t"); return; } public static function lower() { if (OS_WIN) { return; } Console::getOutput()->writeAll("\033[6t"); return; } public static function setTitle($title) { if (OS_WIN) { return; } Console::getOutput()->writeAll("\033]0;" . $title . "\033\\"); return; } public static function getTitle() { if (OS_WIN) { return; } Console::getOutput()->writeAll("\033[21t"); $input = Console::getInput(); $read = [$input->getStream()->getStream()]; $write = []; $except = []; $out = null; if (0 === stream_select($read, $write, $except, 0, 50000)) { return $out; } $input->read(3); do { $char = $input->readCharacter(); if ("\033" === $char) { $chaar = $input->readCharacter(); if ('\\' === $chaar) { break; } $char .= $chaar; } $out .= $char; } while (true); return $out; } public static function getLabel() { if (OS_WIN) { return; } Console::getOutput()->writeAll("\033[20t"); $input = Console::getInput(); $read = [$input->getStream()->getStream()]; $write = []; $except = []; $out = null; if (0 === stream_select($read, $write, $except, 0, 50000)) { return $out; } $input->read(3); do { $char = $input->readCharacter(); if ("\033" === $char) { $chaar = $input->readCharacter(); if ('\\' === $chaar) { break; } $char .= $chaar; } $out .= $char; } while (true); return $out; } public static function refresh() { if (OS_WIN) { return; } Console::getOutput()->writeAll("\033[7t"); return; } public static function copy($data) { if (OS_WIN) { return; } $out = "\033]52;;" . base64_encode($data) . "\033\\"; $output = Console::getOutput(); $considerMultiplexer = $output->considerMultiplexer(true); $output->writeAll($out); $output->considerMultiplexer($considerMultiplexer); return; } } Console::advancedInteraction(); if (function_exists('pcntl_signal')) { Window::getInstance(); pcntl_signal( SIGWINCH, function () { static $_window = null; if (null === $_window) { $_window = Window::getInstance(); } Event::notify( 'hoa://Event/Console/Window:resize', $_window, new Event\Bucket([ 'size' => Window::getSize() ]) ); } ); } setData($data); return; } public function send($eventId, Source $source) { return Event::notify($eventId, $source, $this); } public function setSource(Source $source) { $old = $this->_source; $this->_source = $source; return $old; } public function getSource() { return $this->_source; } public function setData($data) { $old = $this->_data; $this->_data = $data; return $old; } public function getData() { return $this->_data; } } new self(), self::KEY_SOURCE => null ]; } return self::$_register[$eventId][self::KEY_EVENT]; } public static function register($eventId, $source) { if (true === self::eventExists($eventId)) { throw new Exception( 'Cannot redeclare an event with the same ID, i.e. the event ' . 'ID %s already exists.', 0, $eventId ); } if (is_object($source) && !($source instanceof Source)) { throw new Exception( 'The source must implement \Hoa\Event\Source ' . 'interface; given %s.', 1, get_class($source) ); } else { $reflection = new \ReflectionClass($source); if (false === $reflection->implementsInterface('\Hoa\Event\Source')) { throw new Exception( 'The source must implement \Hoa\Event\Source ' . 'interface; given %s.', 2, $source ); } } if (!isset(self::$_register[$eventId][self::KEY_EVENT])) { self::$_register[$eventId][self::KEY_EVENT] = new self(); } self::$_register[$eventId][self::KEY_SOURCE] = $source; return; } public static function unregister($eventId, $hard = false) { if (false !== $hard) { unset(self::$_register[$eventId]); } else { self::$_register[$eventId][self::KEY_SOURCE] = null; } return; } public function attach($callable) { $callable = xcallable($callable); $this->_callable[$callable->getHash()] = $callable; return $this; } public function detach($callable) { unset($this->_callable[xcallable($callable)->getHash()]); return $this; } public function isListened() { return !empty($this->_callable); } public static function notify($eventId, Source $source, Bucket $data) { if (false === self::eventExists($eventId)) { throw new Exception( 'Event ID %s does not exist, cannot send notification.', 3, $eventId ); } $data->setSource($source); $event = self::getEvent($eventId); foreach ($event->_callable as $callable) { $callable($data); } return; } public static function eventExists($eventId) { return array_key_exists($eventId, self::$_register) && self::$_register[$eventId][self::KEY_SOURCE] !== null; } } Consistency::flexEntity('Hoa\Event\Event'); _source = $source; $this->addIds($ids); return; } public function addIds(array $ids) { foreach ($ids as $id) { $this->_callables[$id] = []; } return; } public function attach($listenerId, $callable) { if (false === $this->listenerExists($listenerId)) { throw new Exception( 'Cannot listen %s because it is not defined.', 0, $listenerId ); } $callable = xcallable($callable); $this->_callables[$listenerId][$callable->getHash()] = $callable; return $this; } public function detach($listenerId, $callable) { unset($this->_callables[$listenerId][xcallable($callable)->getHash()]); return $this; } public function detachAll($listenerId) { unset($this->_callables[$listenerId]); return $this; } public function listenerExists($listenerId) { return array_key_exists($listenerId, $this->_callables); } public function fire($listenerId, Bucket $data) { if (false === $this->listenerExists($listenerId)) { throw new Exception( 'Cannot fire on %s because it is not defined.', 1, $listenerId ); } $data->setSource($this->_source); $out = []; foreach ($this->_callables[$listenerId] as $callable) { $out[] = $callable($data); } return $out; } } getListener(); if (null === $listener) { throw new Exception( 'Cannot attach a callable to the listener %s because ' . 'it has not been initialized yet.', 0, get_class($this) ); } $listener->attach($listenerId, $callable); return $this; } protected function setListener(Listener $listener) { $old = $this->_listener; $this->_listener = $listener; return $old; } protected function getListener() { return $this->_listener; } } when($result = new SUT('foo')) ->then ->object($result) ->isInstanceOf('Hoa\Event\Bucket') ->string($result->getData()) ->isEqualTo('foo'); } public function case_send() { $self = $this; $this ->given( $eventId = 'hoa://Event/Test', $source = new \Mock\Hoa\Event\Source(), LUT::register($eventId, $source), $bucket = new SUT('foo'), LUT::getEvent($eventId)->attach( function (SUT $receivedBucket) use ($self, $bucket, &$called) { $called = true; $self ->object($receivedBucket) ->isIdenticalTo($bucket); } ) ) ->when($result = $bucket->send($eventId, $source)) ->then ->variable($result) ->isNull() ->boolean($called) ->isTrue(); } public function case_set_source() { $this ->given( $bucket = new SUT(), $sourceA = new \Mock\Hoa\Event\Source() ) ->when($result = $bucket->setSource($sourceA)) ->then ->variable($result) ->isNull() ->object($bucket->getSource()) ->isIdenticalTo($sourceA) ->given($sourceB = new \Mock\Hoa\Event\Source()) ->when($result = $bucket->setSource($sourceB)) ->then ->object($result) ->isIdenticalTo($sourceA) ->object($bucket->getSource()) ->isIdenticalTo($sourceB); } public function case_set_data() { $this ->given( $bucket = new SUT(), $datumA = 'foo' ) ->when($result = $bucket->setData($datumA)) ->then ->variable($result) ->isNull() ->string($bucket->getData()) ->isEqualTo($datumA) ->given($datumB = 'bar') ->when($result = $bucket->setData($datumB)) ->then ->string($result) ->isEqualTo($datumA) ->string($bucket->getData()) ->isEqualTo($datumB); } } given($eventId = 'hoa://Event/Test') ->when($result = SUT::getEvent($eventId)) ->then ->object($result) ->isInstanceOf('Hoa\Event\Event') ->object(SUT::getEvent($eventId)) ->isIdenticalTo($result); } public function case_register_source_instance() { $this ->given( $eventId = 'hoa://Event/Test', $source = new \Mock\Hoa\Event\Source() ) ->when($result = SUT::register($eventId, $source)) ->then ->variable($result) ->isNull() ->boolean(SUT::eventExists($eventId)) ->isTrue(); } public function case_register_source_name() { $this ->given( $eventId = 'hoa://Event/Test', $source = 'Mock\Hoa\Event\Source' ) ->when($result = SUT::register($eventId, $source)) ->then ->variable($result) ->isNull() ->boolean(SUT::eventExists($eventId)) ->isTrue(); } public function case_register_redeclare() { $this ->given( $eventId = 'hoa://Event/Test', $source = new \Mock\Hoa\Event\Source(), SUT::register($eventId, $source) ) ->exception(function () use ($eventId, $source) { SUT::register($eventId, $source); }) ->isInstanceOf('Hoa\Event\Exception'); } public function case_register_not_a_source_instance() { $this ->given( $eventId = 'hoa://Event/Test', $source = new \StdClass() ) ->exception(function () use ($eventId, $source) { $result = SUT::register($eventId, $source); }) ->isInstanceOf('Hoa\Event\Exception'); } public function case_register_not_a_source_name() { $this ->given( $eventId = 'hoa://Event/Test', $source = 'StdClass' ) ->exception(function () use ($eventId, $source) { $result = SUT::register($eventId, $source); }) ->isInstanceOf('Hoa\Event\Exception'); } public function case_unregister() { $this ->given( $eventId = 'hoa://Event/Test', $source = new \Mock\Hoa\Event\Source(), SUT::register($eventId, $source) ) ->when($result = SUT::unregister($eventId)) ->then ->boolean(SUT::eventExists($eventId)) ->isFalse(); } public function case_unregister_hard() { $this ->given( $eventId = 'hoa://Event/Test', $source = new \Mock\Hoa\Event\Source(), SUT::register($eventId, $source), $event = SUT::getEvent($eventId) ) ->when($result = SUT::unregister($eventId, true)) ->then ->boolean(SUT::eventExists($eventId)) ->isFalse() ->object(SUT::getEvent($eventId)) ->isNotIdenticalTo($event); } public function case_unregister_not_registered() { $this ->given($eventId = 'hoa://Event/Test') ->when($result = SUT::unregister($eventId)) ->then ->variable($result) ->isNull(); } public function case_attach() { $this ->given( $event = SUT::getEvent('hoa://Event/Test'), $callable = function () { } ) ->when($result = $event->attach($callable)) ->then ->object($result) ->isIdenticalTo($event) ->boolean($event->isListened()) ->isTrue(); } public function case_detach() { $this ->given( $event = SUT::getEvent('hoa://Event/Test'), $callable = function () { }, $event->attach($callable) ) ->when($result = $event->detach($callable)) ->then ->object($result) ->isIdenticalTo($event) ->boolean($event->isListened()) ->isFalse(); } public function case_detach_unattached() { $this ->given( $event = SUT::getEvent('hoa://Event/Test'), $callable = function () { } ) ->when($result = $event->detach($callable)) ->then ->object($result) ->isIdenticalTo($event) ->boolean($event->isListened()) ->isFalse(); } public function case_is_listened() { $this ->given($event = SUT::getEvent('hoa://Event/Test')) ->when($result = $event->isListened()) ->then ->boolean($event->isListened()) ->isFalse(); } public function case_notify() { $self = $this; $this ->given( $eventId = 'hoa://Event/Test', $source = new \Mock\Hoa\Event\Source(), $bucket = new LUT\Bucket(), SUT::register($eventId, $source), SUT::getEvent($eventId)->attach( function (LUT\Bucket $receivedBucket) use ($self, $source, $bucket, &$called) { $called = true; $this ->object($receivedBucket) ->isIdenticalTo($bucket) ->object($receivedBucket->getSource()) ->isIdenticalTo($source); } ) ) ->when($result = SUT::notify($eventId, $source, $bucket)) ->then ->variable($result) ->isNull() ->boolean($called) ->isTrue(); } public function case_notify_unregistered_event_id() { $this ->given( $eventId = 'hoa://Event/Test', $source = new \Mock\Hoa\Event\Source(), $data = new LUT\Bucket() ) ->exception(function () use ($eventId, $source, $data) { SUT::notify($eventId, $source, $data); }) ->isInstanceOf('Hoa\Event\Exception'); } public function case_event_exists() { $this ->given( $eventId = 'hoa://Event/Test', $source = new \Mock\Hoa\Event\Source(), SUT::register($eventId, $source) ) ->when($result = SUT::eventExists($eventId)) ->then ->boolean($result) ->isTrue(); } public function case_event_not_exists() { $this ->given($eventId = 'hoa://Event/Test') ->when($result = SUT::eventExists($eventId)) ->then ->boolean($result) ->isFalse(); } } when($result = new SUT('foo', 0)) ->then ->object($result) ->isInstanceOf('Hoa\Exception\Exception'); } } when($result = new \Mock\Hoa\Event\Listenable()) ->then ->object($result) ->isInstanceOf('Hoa\Event\Listenable'); } } given( $source = new \Mock\Hoa\Event\Listenable(), $ids = ['foo', 'bar', 'baz'] ) ->when($result = new SUT($source, $ids)) ->then ->object($result) ->isInstanceOf('Hoa\Event\Listener') ->boolean($result->listenerExists('foo')) ->isTrue() ->boolean($result->listenerExists('bar')) ->isTrue() ->boolean($result->listenerExists('baz')) ->isTrue(); } public function case_attach() { $this ->given( $source = new \Mock\Hoa\Event\Listenable(), $listenerId = 'foo', $listener = new SUT($source, ['foo', 'bar']), $callable = function () { return 42; } ) ->when($result = $listener->attach($listenerId, $callable)) ->then ->object($result) ->isIdenticalTo($listener) ->array($listener->fire($listenerId, new LUT\Bucket())) ->isEqualTo([42]); } public function case_attach_to_an_undefined_listener() { $this ->given( $source = new \Mock\Hoa\Event\Listenable(), $listenerId = 'bar', $listener = new SUT($source, ['foo', 'baz']), $callable = function () { } ) ->exception(function () use ($listener, $listenerId, $callable) { $listener->attach($listenerId, $callable); }) ->isInstanceOf('Hoa\Event\Exception'); } public function case_detach() { $this ->given( $source = new \Mock\Hoa\Event\Listenable(), $listenerId = 'foo', $listener = new SUT($source, ['foo', 'bar']), $callable = function () { return 42; }, $listener->attach($listenerId, $callable) ) ->when($result = $listener->detach($listenerId, $callable)) ->then ->object($result) ->isIdenticalTo($listener) ->array($listener->fire($listenerId, new LUT\Bucket())) ->isEmpty(); } public function case_detach_an_undefined_listener() { $this ->given( $source = new \Mock\Hoa\Event\Listenable(), $listenerId = 'bar', $listener = new SUT($source, ['foo', 'baz']), $callable = function () { } ) ->when($result = $listener->detach($listenerId, $callable)) ->then ->object($result) ->isIdenticalTo($listener); } public function case_detach_all() { $this ->given( $source = new \Mock\Hoa\Event\Listenable(), $listenerId = 'foo', $listener = new SUT($source, ['foo', 'bar']) ) ->when($result = $listener->detachAll($listenerId)) ->then ->object($result) ->isIdenticalTo($listener) ->boolean($listener->listenerExists($listenerId)) ->isFalse(); } public function case_detach_all_with_an_undefined_listener() { $this ->given( $source = new \Mock\Hoa\Event\Listenable(), $listenerId = 'bar', $listener = new SUT($source, ['foo', 'baz']) ) ->when($result = $listener->detachAll($listenerId)) ->then ->object($result) ->isIdenticalTo($listener); } public function case_listener_exists() { $this ->given( $source = new \Mock\Hoa\Event\Listenable(), $ids = [], $listener = new SUT($source, $ids) ) ->when($listener->addIds(['foo'])) ->then ->boolean($listener->listenerExists('foo')) ->isTrue() ->boolean($listener->listenerExists('bar')) ->isFalse() ->when($listener->addIds(['bar'])) ->then ->boolean($listener->listenerExists('bar')) ->isTrue(); } public function case_fire() { $self = $this; $this ->given( $source = new \Mock\Hoa\Event\Listenable(), $ids = ['foo', 'bar'], $listener = new SUT($source, $ids), $listenerId = 'foo', $bucket = new LUT\Bucket(), $listener->attach( $listenerId, function (LUT\Bucket $receivedBucket) use ($self, $bucket, $source, &$called) { $called = true; $self ->object($receivedBucket) ->isIdenticalTo($bucket) ->object($receivedBucket->getSource()) ->isIdenticalTo($source); return 42; } ) ) ->when($result = $listener->fire($listenerId, $bucket)) ->then ->array($result) ->isEqualTo([42]) ->boolean($called) ->isTrue(); } public function case_fire_an_undefined_listenerId() { $this ->given( $source = new \Mock\Hoa\Event\Listenable(), $ids = [], $listener = new SUT($source, $ids) ) ->exception(function () use ($listener) { $listener->fire('foo', new LUT\Bucket()); }) ->isInstanceOf('Hoa\Event\Exception'); } } given( $listenable = new _Listenable(), $listener = new LUT\Listener($listenable, ['foo']) ) ->when($result = $listenable->_setListener($listener)) ->then ->variable($result) ->isNull(); } public function case_get_listener() { $this ->given( $listenable = new _Listenable(), $listener = new LUT\Listener($listenable, ['foo']), $listenable->_setListener($listener) ) ->when($result = $listenable->_getListener()) ->then ->object($result) ->isIdenticalTo($listener); } public function case_on() { $this ->given( $listenable = new _Listenable(), $listener = new LUT\Listener($listenable, ['foo']), $listenable->_setListener($listener), $callable = function () use (&$called) { $called = true; return 42; } ) ->when($result = $listenable->on('foo', $callable)) ->then ->object($result) ->isIdenticalTo($listenable) ->when($listenable->doSomethingThatFires()) ->then ->boolean($called) ->isTrue(); } public function case_on_unregistered_listener() { $this ->given( $listenable = new _Listenable(), $listener = new LUT\Listener($listenable, ['foo']), $listenable->_setListener($listener) ) ->exception(function () use ($listenable) { $listenable->on('bar', null); }) ->isInstanceOf('Hoa\Event\Exception'); } } class _Listenable implements LUT\Listenable { use SUT; public function _setListener(LUT\Listener $listener) { return $this->setListener($listener); } public function _getListener() { return $this->getListener(); } public function doSomethingThatFires() { $this->getListener()->fire('foo', new LUT\Bucket('bar')); return; } } when($result = new \Mock\Hoa\Event\Source()) ->then ->object($result) ->isInstanceOf('Hoa\Event\Source'); } } file = $file; $this->line = $line; $this->_trace = $trace; parent::__construct($message, $code); return; } public static function enableErrorHandler($enable = true) { if (false === $enable) { return restore_error_handler(); } return set_error_handler( function ($no, $str, $file = null, $line = null, $ctx = null) { if (0 === ($no & error_reporting())) { return; } $trace = debug_backtrace(); array_shift($trace); array_shift($trace); throw new Error($str, $no, $file, $line, $trace); } ); } } send(); return; } public function send() { Event::notify( 'hoa://Event/Exception', $this, new Event\Bucket($this) ); return; } } Consistency::flexEntity('Hoa\Exception\Exception'); _group = new \SplStack(); $this->beginTransaction(); return; } public function raise($previous = false) { $out = parent::raise($previous); if (0 >= count($this)) { return $out; } $out .= "\n\n" . 'Contains the following exceptions:'; foreach ($this as $exception) { $out .= "\n\n" . ' • ' . str_replace( "\n", "\n" . ' ', $exception->raise($previous) ); } return $out; } public function beginTransaction() { $this->_group->push(new \ArrayObject()); return $this; } public function rollbackTransaction() { if (1 >= count($this->_group)) { return $this; } $this->_group->pop(); return $this; } public function commitTransaction() { if (false === $this->hasUncommittedExceptions()) { $this->_group->pop(); return $this; } foreach ($this->_group->pop() as $index => $exception) { $this[$index] = $exception; } return $this; } public function hasUncommittedExceptions() { return 1 < count($this->_group) && 0 < count($this->_group->top()); } public function offsetExists($index) { foreach ($this->_group as $group) { if (isset($group[$index])) { return true; } } return false; } public function offsetGet($index) { foreach ($this->_group as $group) { if (isset($group[$index])) { return $group[$index]; } } return null; } public function offsetSet($index, $exception) { if (!($exception instanceof \Exception)) { return null; } $group = $this->_group->top(); if (null === $index || true === is_int($index)) { $group[] = $exception; } else { $group[$index] = $exception; } return; } public function offsetUnset($index) { foreach ($this->_group as $group) { if (isset($group[$index])) { unset($group[$index]); } } return; } public function getExceptions() { return $this->_group->bottom(); } public function getIterator() { return $this->getExceptions()->getIterator(); } public function count() { return count($this->getExceptions()); } public function getStackSize() { return count($this->_group); } } _tmpArguments = $arguments; parent::__construct($message, $code, $previous); $this->_rawMessage = $message; $this->message = @vsprintf($message, $this->getArguments()); return; } public function getBacktrace() { if (null === $this->_trace) { $this->_trace = $this->getTrace(); } return $this->_trace; } public function getPreviousThrow() { if (null === $this->_previous) { $this->_previous = $this->getPrevious(); } return $this->_previous; } public function getArguments() { if (null === $this->_arguments) { $arguments = $this->_tmpArguments; if (!is_array($arguments)) { $arguments = [$arguments]; } foreach ($arguments as &$value) { if (null === $value) { $value = '(null)'; } } $this->_arguments = $arguments; unset($this->_tmpArguments); } return $this->_arguments; } public function getRawMessage() { return $this->_rawMessage; } public function getFormattedMessage() { return $this->getMessage(); } public function getFrom() { $trace = $this->getBacktrace(); $from = '{main}'; if (!empty($trace)) { $t = $trace[0]; $from = ''; if (isset($t['class'])) { $from .= $t['class'] . '::'; } if (isset($t['function'])) { $from .= $t['function'] . '()'; } } return $from; } public function raise($previous = false) { $message = $this->getFormattedMessage(); $trace = $this->getBacktrace(); $file = '/dev/null'; $line = -1; $pre = $this->getFrom(); if (!empty($trace)) { $file = isset($trace['file']) ? $trace['file'] : null; $line = isset($trace['line']) ? $trace['line'] : null; } $pre .= ': '; try { $out = $pre . '(' . $this->getCode() . ') ' . $message . "\n" . 'in ' . $this->getFile() . ' at line ' . $this->getLine() . '.'; } catch (\Exception $e) { $out = $pre . '(' . $this->getCode() . ') ' . $message . "\n" . 'in ' . $file . ' around line ' . $line . '.'; } if (true === $previous && null !== $previous = $this->getPreviousThrow()) { $out .= "\n\n" . ' ⬇' . "\n\n" . 'Nested exception (' . get_class($previous) . '):' . "\n" . ($previous instanceof self ? $previous->raise(true) : $previous->getMessage()); } return $out; } public static function uncaught($exception) { if (!($exception instanceof self)) { throw $exception; } while (0 < ob_get_level()) { ob_end_flush(); } echo 'Uncaught exception (' . get_class($exception) . '):' . "\n" . $exception->raise(true); return; } public function __toString() { return $this->raise(); } public static function enableUncaughtHandler($enable = true) { if (false === $enable) { return restore_exception_handler(); } return set_exception_handler(function ($exception) { return self::uncaught($exception); }); } } when($result = new SUT('foo', 42, '/hoa/flatland', 153)) ->then ->object($result) ->isInstanceOf('Hoa\Exception\Exception'); } public function case_get_message() { $this ->given($exception = new SUT('foo', 42, '/hoa/flatland', 153)) ->when($result = $exception->raise()) ->then ->string($result) ->isEqualTo( '{main}: (42) foo' . "\n" . 'in /hoa/flatland at line 153.' ); } public function case_disable_error_handler() { $this ->given( $this->function->restore_error_handler = function () use (&$called) { $called = true; return null; } ) ->when($result = SUT::enableErrorHandler(false)) ->then ->variable($result) ->isNull() ->boolean($called) ->isTrue(); } public function case_enable_error_handler() { $self = $this; $this ->given( $this->function->set_error_handler = function ($handler) use ($self, &$called) { $called = true; $self ->object($handler) ->isInstanceOf('Closure') ->let($reflection = new \ReflectionObject($handler)) ->array($invokeParameters = $reflection->getMethod('__invoke')->getParameters()) ->hasSize(5) ->string($invokeParameters[0]->getName()) ->isEqualTo('no') ->string($invokeParameters[1]->getName()) ->isEqualTo('str') ->string($invokeParameters[2]->getName()) ->isEqualTo('file') ->boolean($invokeParameters[2]->isOptional()) ->isTrue() ->string($invokeParameters[3]->getName()) ->isEqualTo('line') ->boolean($invokeParameters[3]->isOptional()) ->isTrue() ->string($invokeParameters[4]->getName()) ->isEqualTo('ctx') ->boolean($invokeParameters[4]->isOptional()) ->isTrue(); return null; } ) ->when($result = SUT::enableErrorHandler()) ->then ->variable($result) ->isNull() ->boolean($called) ->isTrue(); } public function case_error_handler() { $this ->given(SUT::enableErrorHandler()) ->exception(function () { ++$i; }) ->isInstanceOf('Hoa\Exception\Error') ->hasMessage('Undefined variable: i'); } } when($result = new SUT('foo')) ->then ->object($result) ->isInstanceOf('Hoa\Exception\Idle'); } public function case_event_is_registered() { $this ->given(new SUT('foo')) ->when($result = Event::eventExists('hoa://Event/Exception')) ->then ->boolean($result) ->isTrue(); } public function case_event_is_sent() { $self = $this; $this ->given( Event::getEvent('hoa://Event/Exception')->attach( function (Event\Bucket $bucket) use ($self, &$called) { $called = true; $self ->object($bucket->getSource()) ->isInstanceOf('Hoa\Exception\Exception') ->string($bucket->getSource()->getMessage()) ->isEqualTo('foo') ->object($bucket->getData()) ->isIdenticalTo($bucket->getSource()); } ) ) ->when(new SUT('foo')) ->then ->boolean($called) ->isTrue(); } } when($result = new SUT('foo')) ->then ->object($result) ->isInstanceOf('Hoa\Exception\Exception') ->isInstanceOf('ArrayAccess') ->isInstanceOf('IteratorAggregate') ->isInstanceOf('Countable'); } public function case_constructor() { $this ->given( $message = 'foo %s %d %s', $code = 7, $arguments = ['arg', 42, null], $previous = new SUT('previous') ) ->when($result = new SUT($message, $code, $arguments, $previous), $line = __LINE__) ->then ->string($result->getMessage()) ->isEqualTo('foo arg 42 (null)') ->integer($result->getCode()) ->isEqualTo(7) ->array($result->getArguments()) ->isEqualTo(['arg', 42, '(null)']) ->object($result->getPreviousThrow()) ->isIdenticalTo($previous) ->boolean($result->hasUncommittedExceptions()) ->isFalse(); } public function case_raise_zero_exception() { $this ->given($group = new SUT('foo'), $line = __LINE__) ->when($result = $group->raise()) ->then ->string($result) ->isEqualTo( __METHOD__ . '(): (0) foo' . "\n" . 'in ' . __FILE__ . ' at line ' . $line . '.' ); } public function case_raise_one_exception() { $this ->given( $exception1 = new SUT('bar'), $barLine = __LINE__, $group = new SUT('foo'), $fooLine = __LINE__, $group[] = $exception1 ) ->when($result = $group->raise()) ->then ->string($result) ->isEqualTo( __METHOD__ . '(): (0) foo' . "\n" . 'in ' . __FILE__ . ' at line ' . $fooLine . '.' . "\n\n" . 'Contains the following exceptions:' . "\n\n" . ' • ' . __METHOD__ . '(): (0) bar' . "\n" . ' in ' . __FILE__ . ' at line ' . $barLine . '.' ); } public function case_raise_more_exceptions() { $this ->given( $exception1 = new SUT('bar'), $barLine = __LINE__, $exception2 = new SUT('baz'), $bazLine = __LINE__, $group = new SUT('foo'), $fooLine = __LINE__, $group[] = $exception1, $group[] = $exception2 ) ->when($result = $group->raise()) ->then ->string($result) ->isEqualTo( __METHOD__ . '(): (0) foo' . "\n" . 'in ' . __FILE__ . ' at line ' . $fooLine . '.' . "\n\n" . 'Contains the following exceptions:' . "\n\n" . ' • ' . __METHOD__ . '(): (0) bar' . "\n" . ' in ' . __FILE__ . ' at line ' . $barLine . '.' . "\n\n" . ' • ' . __METHOD__ . '(): (0) baz' . "\n" . ' in ' . __FILE__ . ' at line ' . $bazLine . '.' ); } public function case_begin_transaction() { $this ->given( $group = new SUT('foo'), $oldStackSize = $group->getStackSize() ) ->when( $result = $group->beginTransaction(), $stackSize = $group->getStackSize() ) ->then ->integer($oldStackSize) ->isEqualTo(1) ->object($result) ->isIdenticalTo($group) ->integer($stackSize) ->isEqualTo($oldStackSize + 1); } public function case_rollback_transaction_with_an_empty_stack() { $this ->given( $group = new SUT('foo'), $oldStackSize = $group->getStackSize() ) ->when( $result = $group->rollbackTransaction(), $stackSize = $group->getStackSize() ) ->then ->integer($oldStackSize) ->isEqualTo(1) ->object($result) ->isIdenticalTo($group) ->integer($stackSize) ->isEqualTo($oldStackSize); } public function case_rollback_transaction() { $this ->given( $group = new SUT('foo'), $group->beginTransaction(), $group->beginTransaction(), $oldStackSize = $group->getStackSize(), $group->rollbackTransaction() ) ->when( $result = $group->rollbackTransaction(), $stackSize = $group->getStackSize() ) ->then ->integer($oldStackSize) ->isEqualTo(3) ->object($result) ->isIdenticalTo($group) ->integer($stackSize) ->isEqualTo($oldStackSize - 2); } public function case_commit_transaction_with_an_empty_stack() { $this ->given( $group = new SUT('foo'), $group->beginTransaction(), $oldCount = count($group), $oldStackSize = $group->getStackSize() ) ->when( $result = $group->commitTransaction(), $count = count($group), $stackSize = $group->getStackSize() ) ->then ->integer($oldCount) ->isEqualTo(0) ->integer($oldStackSize) ->isEqualTo(2) ->object($result) ->isIdenticalTo($group) ->integer($count) ->isEqualTo($oldCount) ->integer($stackSize) ->isEqualTo($oldStackSize - 1); } public function case_commit_transaction() { $this ->given( $group = new SUT('foo'), $group->beginTransaction(), $exception1 = new SUT('bar'), $exception2 = new SUT('baz'), $group[] = $exception1, $group[] = $exception2, $oldCount = count($group), $oldStackSize = $group->getStackSize() ) ->when( $result = $group->commitTransaction(), $count = count($group), $stackSize = $group->getStackSize() ) ->then ->integer($oldCount) ->isEqualTo(0) ->integer($oldStackSize) ->isEqualTo(2) ->object($result) ->isIdenticalTo($group) ->integer($count) ->isEqualTo($oldCount + 2) ->integer($stackSize) ->isEqualTo($oldStackSize - 1) ->array(iterator_to_array($group->getIterator())) ->isEqualTo([ 0 => $exception1, 1 => $exception2 ]); } public function case_has_uncommitted_exceptions() { $this ->given( $group = new SUT('foo'), $group->beginTransaction(), $group[] = new SUT('bar') ) ->when($result = $group->hasUncommittedExceptions()) ->then ->boolean($result) ->isTrue(); } public function case_has_no_uncommitted_exceptions() { $this ->given( $group = new SUT('foo'), $group->beginTransaction() ) ->when($result = $group->hasUncommittedExceptions()) ->then ->boolean($result) ->isFalse(); } public function case_has_no_uncommitted_exceptions_with_empty_stack() { $this ->given( $group = new SUT('foo'), $group[] = new SUT('bar') ) ->when($result = $group->hasUncommittedExceptions()) ->then ->boolean($result) ->isFalse(); } public function case_offset_exists_with_no_uncommited_exceptions() { $this ->given( $group = new SUT('foo'), $group['bar'] = new SUT('bar') ) ->when($result = $group->offsetExists('bar')) ->then ->boolean($result) ->isTrue(); } public function case_offset_does_not_exist_with_no_uncommited_exceptions() { $this ->given( $group = new SUT('foo'), $group['bar'] = new SUT('bar') ) ->when($result = $group->offsetExists('baz')) ->then ->boolean($result) ->isFalse(); } public function case_offset_exists() { $this ->given( $group = new SUT('foo'), $group->beginTransaction(), $group->beginTransaction(), $group['bar'] = new SUT('bar') ) ->when($result = $group->offsetExists('bar')) ->then ->boolean($result) ->isTrue(); } public function case_offset_does_not_exist() { $this ->given( $group = new SUT('foo'), $group->beginTransaction(), $group->beginTransaction(), $group['bar'] = new SUT('bar') ) ->when($result = $group->offsetExists('baz')) ->then ->boolean($result) ->isFalse(); } public function case_offset_get_with_no_uncommited_exceptions() { $this ->given( $group = new SUT('foo'), $exception1 = new SUT('bar'), $group['bar'] = $exception1 ) ->when($result = $group->offsetGet('bar')) ->then ->object($result) ->isIdenticalTo($exception1); } public function case_offset_get_does_not_exist_with_no_uncommited_exceptions() { $this ->given( $group = new SUT('foo'), $exception1 = new SUT('bar'), $group['bar'] = $exception1 ) ->when($result = $group->offsetGet('baz')) ->then ->variable($result) ->isNull(); } public function case_offset_get() { $this ->given( $group = new SUT('foo'), $group->beginTransaction(), $group->beginTransaction(), $exception1 = new SUT('bar'), $group['bar'] = $exception1 ) ->when($result = $group->offsetGet('bar')) ->then ->object($result) ->isIdenticalTo($exception1); } public function case_offset_get_does_not_exist() { $this ->given( $group = new SUT('foo'), $group->beginTransaction(), $group->beginTransaction(), $exception1 = new SUT('bar'), $group['bar'] = $exception1 ) ->when($result = $group->offsetGet('baz')) ->then ->variable($result) ->isNull(); } public function case_offset_set_not_an_exception() { $this ->given($group = new SUT('foo')) ->when($group->offsetSet('bar', new \StdClass())) ->then ->boolean($group->offsetExists('bar')) ->isFalse(); } public function case_offset_set() { $this ->given( $group = new SUT('foo'), $exception1 = new SUT('bar') ) ->when($result = $group->offsetExists('bar')) ->then ->boolean($result) ->isFalse() ->when($group->offsetSet('bar', $exception1)) ->then ->boolean($group->offsetExists('bar')) ->isTrue() ->object($group->offsetGet('bar')) ->isIdenticalTo($exception1); } public function case_offset_set_with_a_null_index() { $this ->given( $group = new SUT('foo'), $exception1 = new SUT('bar') ) ->when($group->offsetSet(null, $exception1)) ->then ->boolean($group->offsetExists(0)) ->isTrue() ->object($group->offsetGet(0)) ->isIdenticalTo($exception1); } public function case_offset_set_with_an_integer_index() { $this ->given( $group = new SUT('foo'), $exception1 = new SUT('bar') ) ->when($group->offsetSet(42, $exception1)) ->then ->boolean($group->offsetExists(42)) ->isFalse() ->boolean($group->offsetExists(0)) ->isTrue() ->object($group->offsetGet(0)) ->isIdenticalTo($exception1); } public function case_offset_unset_with_no_uncommited_exceptions() { $this ->given( $group = new SUT('foo'), $group['bar'] = new SUT('bar') ) ->when($group->offsetUnset('bar')) ->then ->boolean($group->offsetExists('bar')) ->isFalse(); } public function case_offset_unset_does_not_exist_with_no_uncommited_exceptions() { $this ->given($group = new SUT('foo')) ->when($group->offsetUnset('bar')) ->then ->boolean($group->offsetExists('bar')) ->isFalse(); } public function case_offset_unset() { $this ->given( $group = new SUT('foo'), $group->beginTransaction(), $group->beginTransaction(), $group['bar'] = new SUT('bar') ) ->when($result = $group->offsetUnset('bar')) ->then ->boolean($group->offsetExists('bar')) ->isFalse(); } public function case_offset_unset_does_not_exist() { $this ->given( $group = new SUT('foo'), $group->beginTransaction(), $group->beginTransaction() ) ->when($result = $group->offsetUnset('bar')) ->then ->boolean($group->offsetExists('bar')) ->isFalse(); } public function case_get_exceptions() { $this ->given( $group = new SUT('foo'), $exception1 = new SUT('bar'), $exception2 = new SUT('baz'), $group['bar'] = $exception1, $group->beginTransaction(), $group['baz'] = $exception2 ) ->when($result = $group->getExceptions()) ->then ->object($result) ->isInstanceOf('ArrayObject') ->object($result['bar']) ->isIdenticalTo($exception1); } public function case_get_iterator() { $this ->given( $group = new SUT('foo'), $exception1 = new SUT('bar'), $group['bar'] = $exception1 ) ->when($result = $group->getIterator()) ->then ->object($result) ->isInstanceOf('ArrayIterator') ->array(iterator_to_array($result)) ->isEqualTo([ 'bar' => $exception1 ]); } public function case_count() { $this ->given( $group = new SUT('foo'), $exception1 = new SUT('bar'), $exception2 = new SUT('baz'), $group['bar'] = $exception1, $group->beginTransaction(), $group['baz'] = $exception2 ) ->when($result = count($group)) ->then ->integer($result) ->isEqualTo(1); } public function get_get_stack_size() { $this ->given( $group = new SUT('foo'), $exception1 = new SUT('bar'), $exception2 = new SUT('baz'), $group['bar'] = $exception1, $group->beginTransaction(), $group['baz'] = $exception2 ) ->when($result = $group->getStackSize()) ->then ->integer($result) ->isEqualTo(2); } } when($result = new SUT('foo')) ->then ->object($result) ->isInstanceOf('Exception'); } public function case_get_backtrace() { $this ->given($exception = new SUT('foo')) ->when($result = $exception->getBacktrace()) ->then ->array($result) ->hasKey(0) ->array($result[0]) ->hasKey('file') ->hasKey('line') ->hasKey('function') ->hasKey('class') ->hasKey('type') ->hasKey('args'); } public function case_get_previous_throw() { $this ->given( $previous = new SUT('previous'), $exception = new SUT('foo', 0, [], $previous) ) ->when($result = $exception->getPreviousThrow()) ->then ->object($result) ->isIdenticalTo($previous); } public function case_get_arguments() { $this ->given($exception = new SUT('foo', 0, ['arg', 42, null])) ->when($result = $exception->getArguments()) ->then ->array($result) ->isEqualTo(['arg', 42, '(null)']); } public function case_get_arguments_from_a_string() { $this ->given($exception = new SUT('foo', 0, 'arg')) ->when($result = $exception->getArguments()) ->then ->array($result) ->isEqualTo(['arg']); } public function case_get_raw_message() { $this ->given( $message = 'foo %s', $exception = new SUT($message) ) ->when($result = $exception->getRawMessage()) ->then ->string($result) ->isEqualTo($message); } public function case_get_formatted_message() { $this ->given( $message = 'foo %s', $exception = new SUT($message, 0, 'bar') ) ->when($result = $exception->getFormattedMessage()) ->then ->string($result) ->isEqualTo($exception->getMessage()) ->isEqualTo('foo bar'); } public function case_get_from_object() { $this ->given($exception = new SUT('foo')) ->when($result = $exception->getFrom()) ->then ->string($result) ->isEqualTo(__METHOD__ . '()'); } public function case_raise() { $this ->given($exception = new SUT('foo'), $line = __LINE__) ->when($result = $exception->raise()) ->then ->string($result) ->isEqualTo( __METHOD__ . '(): (0) foo' . "\n" . 'in ' . __FILE__ . ' at line ' . $line . '.' ); } public function case_raise_with_previous() { $this ->given( $previous = new SUT('previous'), $previousLine = __LINE__, $exception = new SUT('foo', 0, [], $previous), $line = __LINE__ ) ->when($result = $exception->raise(true)) ->then ->string($result) ->isEqualTo( __METHOD__ . '(): (0) foo' . "\n" . 'in ' . __FILE__ . ' at line ' . $line . '.' . "\n\n" . ' ⬇' . "\n\n" . 'Nested exception (' . get_class($previous) . '):' . "\n" . __METHOD__ . '(): (0) previous' . "\n" . 'in ' . __FILE__ . ' at line ' . $previousLine . '.' ); } public function case_uncaught() { $this ->given( $this->function->ob_get_level = 0, $exception = new SUT('foo'), $line = __LINE__ ) ->when($result = SUT::uncaught($exception)) ->then ->variable($result) ->isNull() ->output ->isEqualTo( 'Uncaught exception (' . get_class($exception) . '):' . "\n" . __METHOD__ . '(): (0) foo' . "\n" . 'in ' . __FILE__ . ' at line ' . $line . '.' ); } public function case_uncaught_not_Hoa() { $this ->exception(function () { SUT::uncaught(new \Exception('foo')); }) ->isInstanceOf('Exception') ->output ->isEmpty(); } public function case_to_string() { $this ->given($exception = new SUT('foo')) ->when($result = $exception->__toString()) ->then ->string($result) ->isEqualTo($exception->raise()); } public function case_disable_uncaught_handler() { $this ->given( $this->function->restore_exception_handler = function () use (&$called) { $called = true; return null; } ) ->when($result = SUT::enableUncaughtHandler(false)) ->then ->variable($result) ->isNull() ->boolean($called) ->isTrue(); } public function case_enable_uncaught_handler() { $self = $this; $this ->given( $this->function->set_exception_handler = function ($handler) use ($self, &$called) { $called = true; $self ->object($handler) ->isInstanceOf('Closure') ->let($reflection = new \ReflectionObject($handler)) ->array($invokeParameters = $reflection->getMethod('__invoke')->getParameters()) ->hasSize(1) ->string($invokeParameters[0]->getName()) ->isEqualTo('exception'); return null; } ) ->when($result = SUT::enableUncaughtHandler()) ->then ->variable($result) ->isNull() ->boolean($called) ->isTrue(); } } setMode($mode); parent::__construct($streamName, $context, $wait); return; } protected function &_open($streamName, Stream\Context $context = null) { if (false === is_dir($streamName)) { if ($this->getMode() == self::MODE_READ) { throw new Exception\FileDoesNotExist( 'Directory %s does not exist.', 0, $streamName ); } else { self::create( $streamName, $this->getMode(), null !== $context ? $context->getContext() : null ); } } $out = null; return $out; } protected function _close() { return true; } public function copy($to, $force = Stream\IStream\Touchable::DO_NOT_OVERWRITE) { if (empty($to)) { throw new Exception( 'The destination path (to copy) is empty.', 1 ); } $from = $this->getStreamName(); $fromLength = strlen($from) + 1; $finder = new Finder(); $finder->in($from); self::create($to, self::MODE_CREATE_RECURSIVE); foreach ($finder as $file) { $relative = substr($file->getPathname(), $fromLength); $_to = $to . DS . $relative; if (true === $file->isDir()) { self::create($_to, self::MODE_CREATE); continue; } $handle = null; if (true === $file->isFile()) { $handle = new Read($file->getPathname()); } elseif (true === $file->isDir()) { $handle = new Directory($file->getPathName()); } elseif (true === $file->isLink()) { $handle = new Link\Read($file->getPathName()); } if (null !== $handle) { $handle->copy($_to, $force); $handle->close(); } } return true; } public function delete() { $from = $this->getStreamName(); $finder = new Finder(); $finder->in($from) ->childFirst(); foreach ($finder as $file) { $file->open()->delete(); $file->close(); } if (null === $this->getStreamContext()) { return @rmdir($from); } return @rmdir($from, $this->getStreamContext()->getContext()); } public static function create( $name, $mode = self::MODE_CREATE_RECURSIVE, $context = null ) { if (true === is_dir($name)) { return true; } if (empty($name)) { return false; } if (null !== $context) { if (false === Stream\Context::contextExists($context)) { throw new Exception( 'Context %s was not previously declared, cannot retrieve ' . 'this context.', 2, $context ); } else { $context = Stream\Context::getInstance($context); } } if (null === $context) { return @mkdir( $name, 0755, self::MODE_CREATE_RECURSIVE === $mode ); } return @mkdir( $name, 0755, self::MODE_CREATE_RECURSIVE === $mode, $context->getContext() ); } } setMode($mode); switch ($streamName) { case '0': $streamName = 'php://stdin'; break; case '1': $streamName = 'php://stdout'; break; case '2': $streamName = 'php://stderr'; break; default: if (true === ctype_digit($streamName)) { if (PHP_VERSION_ID >= 50306) { $streamName = 'php://fd/' . $streamName; } else { throw new Exception( 'You need PHP5.3.6 to use a file descriptor ' . 'other than 0, 1 or 2 (tried %d with PHP%s).', 0, [$streamName, PHP_VERSION] ); } } } parent::__construct($streamName, $context, $wait); return; } protected function &_open($streamName, Stream\Context $context = null) { if (substr($streamName, 0, 4) == 'file' && false === is_dir(dirname($streamName))) { throw new Exception( 'Directory %s does not exist. Could not open file %s.', 1, [dirname($streamName), basename($streamName)] ); } if (null === $context) { if (false === $out = @fopen($streamName, $this->getMode(), true)) { throw new Exception( 'Failed to open stream %s.', 2, $streamName ); } return $out; } $out = @fopen( $streamName, $this->getMode(), true, $context->getContext() ); if (false === $out) { throw new Exception( 'Failed to open stream %s.', 3, $streamName ); } return $out; } protected function _close() { return @fclose($this->getStream()); } public function newBuffer($callable = null, $size = null) { $this->setStreamBuffer($size); return 1; } public function flush() { return fflush($this->getStream()); } public function deleteBuffer() { return $this->disableStreamBuffer(); } public function getBufferLevel() { return 1; } public function getBufferSize() { return $this->getStreamBufferSize(); } public function lock($operation) { return flock($this->getStream(), $operation); } public function rewind() { return rewind($this->getStream()); } public function seek($offset, $whence = Stream\IStream\Pointable::SEEK_SET) { return fseek($this->getStream(), $offset, $whence); } public function tell() { $stream = $this->getStream(); if (null === $stream) { return 0; } return ftell($stream); } public static function create($name, $dummy) { if (file_exists($name)) { return true; } return touch($name); } } Consistency::flexEntity('Hoa\File\File'); _flags = Iterator\FileSystem::KEY_AS_PATHNAME | Iterator\FileSystem::CURRENT_AS_FILEINFO | Iterator\FileSystem::SKIP_DOTS; $this->_first = Iterator\Recursive\Iterator::SELF_FIRST; return; } public function in($paths) { if (!is_array($paths)) { $paths = [$paths]; } foreach ($paths as $path) { if (1 === preg_match('/[\*\?\[\]]/', $path)) { $iterator = new Iterator\CallbackFilter( new Iterator\Glob(rtrim($path, DS)), function ($current) { return $current->isDir(); } ); foreach ($iterator as $fileInfo) { $this->_paths[] = $fileInfo->getPathname(); } } else { $this->_paths[] = $path; } } return $this; } public function maxDepth($depth) { $this->_maxDepth = $depth; return $this; } public function files() { $this->_types[] = 'file'; return $this; } public function directories() { $this->_types[] = 'dir'; return $this; } public function links() { $this->_types[] = 'link'; return $this; } public function followSymlinks($flag = true) { if (true === $flag) { $this->_flags ^= Iterator\FileSystem::FOLLOW_SYMLINKS; } else { $this->_flags |= Iterator\FileSystem::FOLLOW_SYMLINKS; } return $this; } public function name($regex) { $this->_filters[] = function (\SplFileInfo $current) use ($regex) { return 0 !== preg_match($regex, $current->getBasename()); }; return $this; } public function notIn($regex) { $this->_filters[] = function (\SplFileInfo $current) use ($regex) { foreach (explode(DS, $current->getPathname()) as $part) { if (0 !== preg_match($regex, $part)) { return false; } } return true; }; return $this; } public function size($size) { if (0 === preg_match('#^(<|<=|>|>=|=)\s*(\d+)\s*((?:[KMGTPEZY])b)?$#', $size, $matches)) { return $this; } $number = floatval($matches[2]); $unit = isset($matches[3]) ? $matches[3] : 'b'; $operator = $matches[1]; switch ($unit) { case 'b': break; case 'Kb': $number <<= 10; break; case 'Mb': $number <<= 20; break; case 'Gb': $number <<= 30; break; case 'Tb': $number *= 1099511627776; break; case 'Pb': $number *= pow(1024, 5); break; case 'Eb': $number *= pow(1024, 6); break; case 'Zb': $number *= pow(1024, 7); break; case 'Yb': $number *= pow(1024, 8); break; } $filter = null; switch ($operator) { case '<': $filter = function (\SplFileInfo $current) use ($number) { return $current->getSize() < $number; }; break; case '<=': $filter = function (\SplFileInfo $current) use ($number) { return $current->getSize() <= $number; }; break; case '>': $filter = function (\SplFileInfo $current) use ($number) { return $current->getSize() > $number; }; break; case '>=': $filter = function (\SplFileInfo $current) use ($number) { return $current->getSize() >= $number; }; break; case '=': $filter = function (\SplFileInfo $current) use ($number) { return $current->getSize() === $number; }; break; } $this->_filters[] = $filter; return $this; } public function dots($flag = true) { if (true === $flag) { $this->_flags ^= Iterator\FileSystem::SKIP_DOTS; } else { $this->_flags |= Iterator\FileSystem::SKIP_DOTS; } return $this; } public function owner($owner) { $this->_filters[] = function (\SplFileInfo $current) use ($owner) { return $current->getOwner() === $owner; }; return $this; } protected function formatDate($date, &$operator) { $operator = -1; if (0 === preg_match('#\bago\b#', $date)) { $date .= ' ago'; } if (0 !== preg_match('#^(since|until)\b(.+)$#', $date, $matches)) { $time = strtotime($matches[2]); if ('until' === $matches[1]) { $operator = 1; } } else { $time = strtotime($date); } return $time; } public function changed($date) { $time = $this->formatDate($date, $operator); if (-1 === $operator) { $this->_filters[] = function (\SplFileInfo $current) use ($time) { return $current->getCTime() >= $time; }; } else { $this->_filters[] = function (\SplFileInfo $current) use ($time) { return $current->getCTime() < $time; }; } return $this; } public function modified($date) { $time = $this->formatDate($date, $operator); if (-1 === $operator) { $this->_filters[] = function (\SplFileInfo $current) use ($time) { return $current->getMTime() >= $time; }; } else { $this->_filters[] = function (\SplFileInfo $current) use ($time) { return $current->getMTime() < $time; }; } return $this; } public function filter($callback) { $this->_filters[] = $callback; return $this; } public function sortByName($locale = 'root') { if (true === class_exists('Collator', false)) { $collator = new \Collator($locale); $this->_sorts[] = function (\SplFileInfo $a, \SplFileInfo $b) use ($collator) { return $collator->compare($a->getPathname(), $b->getPathname()); }; } else { $this->_sorts[] = function (\SplFileInfo $a, \SplFileInfo $b) { return strcmp($a->getPathname(), $b->getPathname()); }; } return $this; } public function sortBySize() { $this->_sorts[] = function (\SplFileInfo $a, \SplFileInfo $b) { return $a->getSize() < $b->getSize(); }; return $this; } public function sort($callable) { $this->_sorts[] = $callable; return $this; } public function childFirst() { $this->_first = Iterator\Recursive\Iterator::CHILD_FIRST; return $this; } public function getIterator() { $_iterator = new Iterator\Append(); $types = $this->getTypes(); if (!empty($types)) { $this->_filters[] = function (\SplFileInfo $current) use ($types) { return in_array($current->getType(), $types); }; } $maxDepth = $this->getMaxDepth(); $splFileInfo = $this->getSplFileInfo(); foreach ($this->getPaths() as $path) { if (1 == $maxDepth) { $iterator = new Iterator\IteratorIterator( new Iterator\Recursive\Directory( $path, $this->getFlags(), $splFileInfo ), $this->getFirst() ); } else { $iterator = new Iterator\Recursive\Iterator( new Iterator\Recursive\Directory( $path, $this->getFlags(), $splFileInfo ), $this->getFirst() ); if (1 < $maxDepth) { $iterator->setMaxDepth($maxDepth - 1); } } $_iterator->append($iterator); } foreach ($this->getFilters() as $filter) { $_iterator = new Iterator\CallbackFilter( $_iterator, $filter ); } $sorts = $this->getSorts(); if (empty($sorts)) { return $_iterator; } $array = iterator_to_array($_iterator); foreach ($sorts as $sort) { uasort($array, $sort); } return new Iterator\Map($array); } public function setSplFileInfo($splFileInfo) { $old = $this->_splFileInfo; $this->_splFileInfo = $splFileInfo; return $old; } public function getSplFileInfo() { return $this->_splFileInfo; } protected function getPaths() { return $this->_paths; } public function getMaxDepth() { return $this->_maxDepth; } public function getTypes() { return $this->_types; } protected function getFilters() { return $this->_filters; } protected function getSorts() { return $this->_sorts; } public function getFlags() { return $this->_flags; } public function getFirst() { return $this->_first; } } getStreamName()); } public function getDirname() { return dirname($this->getStreamName()); } public function getSize() { if (false === $this->getStatistic()) { return false; } return filesize($this->getStreamName()); } public function getStatistic() { return fstat($this->getStream()); } public function getATime() { return fileatime($this->getStreamName()); } public function getCTime() { return filectime($this->getStreamName()); } public function getMTime() { return filemtime($this->getStreamName()); } public function getGroup() { return filegroup($this->getStreamName()); } public function getOwner() { return fileowner($this->getStreamName()); } public function getPermissions() { return fileperms($this->getStreamName()); } public function getReadablePermissions() { $p = $this->getPermissions(); if (($p & 0xC000) == 0xC000) { $out = 's'; } elseif (($p & 0xA000) == 0xA000) { $out = 'l'; } elseif (($p & 0x8000) == 0x8000) { $out = '-'; } elseif (($p & 0x6000) == 0x6000) { $out = 'b'; } elseif (($p & 0x4000) == 0x4000) { $out = 'd'; } elseif (($p & 0x2000) == 0x2000) { $out = 'c'; } elseif (($p & 0x1000) == 0x1000) { $out = 'p'; } else { $out = 'u'; } $out .= (($p & 0x0100) ? 'r' : '-') . (($p & 0x0080) ? 'w' : '-') . (($p & 0x0040) ? (($p & 0x0800) ? 's' : 'x') : (($p & 0x0800) ? 'S' : '-')) . (($p & 0x0020) ? 'r' : '-') . (($p & 0x0010) ? 'w' : '-') . (($p & 0x0008) ? (($p & 0x0400) ? 's' : 'x') : (($p & 0x0400) ? 'S' : '-')) . (($p & 0x0004) ? 'r' : '-') . (($p & 0x0002) ? 'w' : '-') . (($p & 0x0001) ? (($p & 0x0200) ? 't' : 'x') : (($p & 0x0200) ? 'T' : '-')); return $out; } public function isReadable() { return is_readable($this->getStreamName()); } public function isWritable() { return is_writable($this->getStreamName()); } public function isExecutable() { return is_executable($this->getStreamName()); } public function clearStatisticCache() { clearstatcache(true, $this->getStreamName()); return; } public static function clearAllStatisticCaches() { clearstatcache(); return; } public function touch($time = -1, $atime = -1) { if ($time == -1) { $time = time(); } if ($atime == -1) { $atime = $time; } return touch($this->getStreamName(), $time, $atime); } public function copy($to, $force = Stream\IStream\Touchable::DO_NOT_OVERWRITE) { $from = $this->getStreamName(); if ($force === Stream\IStream\Touchable::DO_NOT_OVERWRITE && true === file_exists($to)) { return true; } if (null === $this->getStreamContext()) { return @copy($from, $to); } return @copy($from, $to, $this->getStreamContext()->getContext()); } public function move( $name, $force = Stream\IStream\Touchable::DO_NOT_OVERWRITE, $mkdir = Stream\IStream\Touchable::DO_NOT_MAKE_DIRECTORY ) { $from = $this->getStreamName(); if ($force === Stream\IStream\Touchable::DO_NOT_OVERWRITE && true === file_exists($name)) { return false; } if (Stream\IStream\Touchable::MAKE_DIRECTORY === $mkdir) { Directory::create( dirname($name), Directory::MODE_CREATE_RECURSIVE ); } if (null === $this->getStreamContext()) { return @rename($from, $name); } return @rename($from, $name, $this->getStreamContext()->getContext()); } public function delete() { if (null === $this->getStreamContext()) { return @unlink($this->getStreamName()); } return @unlink( $this->getStreamName(), $this->getStreamContext()->getContext() ); } public function changeGroup($group) { return chgrp($this->getStreamName(), $group); } public function changeMode($mode) { return chmod($this->getStreamName(), $mode); } public function changeOwner($user) { return chown($this->getStreamName(), $user); } public static function umask($umask = null) { if (null === $umask) { return umask(); } return umask($umask); } public function isFile() { return is_file($this->getStreamName()); } public function isLink() { return is_link($this->getStreamName()); } public function isDirectory() { return is_dir($this->getStreamName()); } public function isSocket() { return filetype($this->getStreamName()) == 'socket'; } public function isFIFOPipe() { return filetype($this->getStreamName()) == 'fifo'; } public function isCharacterSpecial() { return filetype($this->getStreamName()) == 'char'; } public function isBlockSpecial() { return filetype($this->getStreamName()) == 'block'; } public function isUnknown() { return filetype($this->getStreamName()) == 'unknown'; } protected function setMode($mode) { $old = $this->_mode; $this->_mode = $mode; return $old; } public function getMode() { return $this->_mode; } public function getINode() { return fileinode($this->getStreamName()); } public static function isCaseSensitive() { return !( file_exists(mb_strtolower(__FILE__)) && file_exists(mb_strtoupper(__FILE__)) ); } public function getRealPath() { if (false === $out = realpath($this->getStreamName())) { return $this->getStreamName(); } return $out; } public function getExtension() { return pathinfo( $this->getStreamName(), PATHINFO_EXTENSION ); } public function getFilename() { $file = basename($this->getStreamName()); if (defined('PATHINFO_FILENAME')) { return pathinfo($file, PATHINFO_FILENAME); } if (strstr($file, '.')) { return substr($file, 0, strrpos($file, '.')); } return $file; } } getStreamName()); } public function changeGroup($group) { return lchgrp($this->getStreamName(), $group); } public function changeOwner($user) { return lchown($this->getStreamName(), $user); } public function getPermissions() { return 41453; } public function getTarget() { $target = dirname($this->getStreamName()) . DS . $this->getTargetName(); $context = null !== $this->getStreamContext() ? $this->getStreamContext()->getCurrentId() : null; if (true === is_link($target)) { return new ReadWrite( $target, File::MODE_APPEND_READ_WRITE, $context ); } elseif (true === is_file($target)) { return new File\ReadWrite( $target, File::MODE_APPEND_READ_WRITE, $context ); } elseif (true === is_dir($target)) { return new File\Directory( $target, File::MODE_READ, $context ); } throw new File\Exception( 'Cannot find an appropriated object that matches with ' . 'path %s when defining it.', 1, $target ); } public function getTargetName() { return readlink($this->getStreamName()); } public static function create($name, $target) { if (false != linkinfo($name)) { return true; } return symlink($target, $name); } } Consistency::flexEntity('Hoa\File\Link\Link'); getMode(), $createModes)) { throw new File\Exception( 'Open mode are not supported; given %d. Only %s are supported.', 0, [$this->getMode(), implode(', ', $createModes)] ); } preg_match('#^(\w+)://#', $streamName, $match); if (((isset($match[1]) && $match[1] == 'file') || !isset($match[1])) && !file_exists($streamName)) { throw new File\Exception\FileDoesNotExist( 'File %s does not exist.', 1, $streamName ); } $out = parent::_open($streamName, $context); return $out; } public function eof() { return feof($this->getStream()); } public function read($length) { if (0 > $length) { throw new File\Exception( 'Length must be greater than 0, given %d.', 2, $length ); } return fread($this->getStream(), $length); } public function readString($length) { return $this->read($length); } public function readCharacter() { return fgetc($this->getStream()); } public function readBoolean() { return (bool) $this->read(1); } public function readInteger($length = 1) { return (int) $this->read($length); } public function readFloat($length = 1) { return (float) $this->read($length); } public function readArray($format = null) { return $this->scanf($format); } public function readLine() { return fgets($this->getStream()); } public function readAll($offset = 0) { return stream_get_contents($this->getStream(), -1, $offset); } public function scanf($format) { return fscanf($this->getStream(), $format); } } getMode(), $createModes)) { throw new File\Exception( 'Open mode are not supported; given %d. Only %s are supported.', 0, [$this->getMode(), implode(', ', $createModes)] ); } preg_match('#^(\w+)://#', $streamName, $match); if (((isset($match[1]) && $match[1] == 'file') || !isset($match[1])) && !file_exists($streamName) && parent::MODE_READ_WRITE == $this->getMode()) { throw new File\Exception\FileDoesNotExist( 'File %s does not exist.', 1, $streamName ); } $out = parent::_open($streamName, $context); return $out; } public function eof() { return feof($this->getStream()); } public function read($length) { if (0 > $length) { throw new File\Exception( 'Length must be greater than 0, given %d.', 2, $length ); } return fread($this->getStream(), $length); } public function readString($length) { return $this->read($length); } public function readCharacter() { return fgetc($this->getStream()); } public function readBoolean() { return (bool) $this->read(1); } public function readInteger($length = 1) { return (int) $this->read($length); } public function readFloat($length = 1) { return (float) $this->read($length); } public function readArray($format = null) { return $this->scanf($format); } public function readLine() { return fgets($this->getStream()); } public function readAll($offset = 0) { return stream_get_contents($this->getStream(), -1, $offset); } public function scanf($format) { return fscanf($this->getStream(), $format); } public function write($string, $length) { if (0 > $length) { throw new File\Exception( 'Length must be greater than 0, given %d.', 3, $length ); } return fwrite($this->getStream(), $string, $length); } public function writeString($string) { $string = (string) $string; return $this->write($string, strlen($string)); } public function writeCharacter($char) { return $this->write((string) $char[0], 1); } public function writeBoolean($boolean) { return $this->write((string) (bool) $boolean, 1); } public function writeInteger($integer) { $integer = (string) (int) $integer; return $this->write($integer, strlen($integer)); } public function writeFloat($float) { $float = (string) (float) $float; return $this->write($float, strlen($float)); } public function writeArray(array $array) { $array = var_export($array, true); return $this->write($array, strlen($array)); } public function writeLine($line) { if (false === $n = strpos($line, "\n")) { return $this->write($line . "\n", strlen($line) + 1); } ++$n; return $this->write(substr($line, 0, $n), $n); } public function writeAll($string) { return $this->write($string, strlen($string)); } public function truncate($size) { return ftruncate($this->getStream(), $size); } } getMode(), $createModes)) { throw new File\Exception( 'Open mode are not supported; given %d. Only %s are supported.', 0, [$this->getMode(), implode(', ', $createModes)] ); } preg_match('#^(\w+)://#', $streamName, $match); if (((isset($match[1]) && $match[1] == 'file') || !isset($match[1])) && !file_exists($streamName)) { throw new File\Exception\FileDoesNotExist( 'File %s does not exist.', 1, $streamName ); } $out = parent::_open($streamName, $context); return $out; } public function write($string, $length) { if (0 > $length) { throw new File\Exception( 'Length must be greater than 0, given %d.', 2, $length ); } return fwrite($this->getStream(), $string, $length); } public function writeString($string) { $string = (string) $string; return $this->write($string, strlen($string)); } public function writeCharacter($char) { return $this->write((string) $char[0], 1); } public function writeBoolean($boolean) { return $this->write((string) (bool) $boolean, 1); } public function writeInteger($integer) { $integer = (string) (int) $integer; return $this->write($integer, strlen($integer)); } public function writeFloat($float) { $float = (string) (float) $float; return $this->write($float, strlen($float)); } public function writeArray(array $array) { $array = var_export($array, true); return $this->write($array, strlen($array)); } public function writeLine($line) { if (false === $n = strpos($line, "\n")) { return $this->write($line . "\n", strlen($line) + 1); } ++$n; return $this->write(substr($line, 0, $n), $n); } public function writeAll($string) { return $this->write($string, strlen($string)); } public function truncate($size) { return ftruncate($this->getStream(), $size); } } getMode(), $createModes)) { throw new Exception( 'Open mode are not supported; given %d. Only %s are supported.', 0, [$this->getMode(), implode(', ', $createModes)] ); } preg_match('#^(\w+)://#', $streamName, $match); if (((isset($match[1]) && $match[1] == 'file') || !isset($match[1])) && !file_exists($streamName)) { throw new Exception\FileDoesNotExist( 'File %s does not exist.', 1, $streamName ); } $out = parent::_open($streamName, $context); return $out; } public function eof() { return feof($this->getStream()); } public function read($length) { if (0 > $length) { throw new Exception( 'Length must be greater than 0, given %d.', 2, $length ); } return fread($this->getStream(), $length); } public function readString($length) { return $this->read($length); } public function readCharacter() { return fgetc($this->getStream()); } public function readBoolean() { return (bool) $this->read(1); } public function readInteger($length = 1) { return (int) $this->read($length); } public function readFloat($length = 1) { return (float) $this->read($length); } public function readArray($format = null) { return $this->scanf($format); } public function readLine() { return fgets($this->getStream()); } public function readAll($offset = 0) { return stream_get_contents($this->getStream(), -1, $offset); } public function scanf($format) { return fscanf($this->getStream(), $format); } } getMode(), $createModes)) { throw new Exception( 'Open mode are not supported; given %d. Only %s are supported.', 0, [$this->getMode(), implode(', ', $createModes)] ); } preg_match('#^(\w+)://#', $streamName, $match); if (((isset($match[1]) && $match[1] == 'file') || !isset($match[1])) && !file_exists($streamName) && parent::MODE_READ_WRITE == $this->getMode()) { throw new Exception\FileDoesNotExist( 'File %s does not exist.', 1, $streamName ); } $out = parent::_open($streamName, $context); return $out; } public function eof() { return feof($this->getStream()); } public function read($length) { if (0 > $length) { throw new Exception( 'Length must be greater than 0, given %d.', 2, $length ); } return fread($this->getStream(), $length); } public function readString($length) { return $this->read($length); } public function readCharacter() { return fgetc($this->getStream()); } public function readBoolean() { return (bool) $this->read(1); } public function readInteger($length = 1) { return (int) $this->read($length); } public function readFloat($length = 1) { return (float) $this->read($length); } public function readArray($format = null) { return $this->scanf($format); } public function readLine() { return fgets($this->getStream()); } public function readAll($offset = 0) { return stream_get_contents($this->getStream(), -1, $offset); } public function scanf($format) { return fscanf($this->getStream(), $format); } public function write($string, $length) { if (0 > $length) { throw new Exception( 'Length must be greater than 0, given %d.', 3, $length ); } return fwrite($this->getStream(), $string, $length); } public function writeString($string) { $string = (string) $string; return $this->write($string, strlen($string)); } public function writeCharacter($char) { return $this->write((string) $char[0], 1); } public function writeBoolean($boolean) { return $this->write((string) (bool) $boolean, 1); } public function writeInteger($integer) { $integer = (string) (int) $integer; return $this->write($integer, strlen($integer)); } public function writeFloat($float) { $float = (string) (float) $float; return $this->write($float, strlen($float)); } public function writeArray(array $array) { $array = var_export($array, true); return $this->write($array, strlen($array)); } public function writeLine($line) { if (false === $n = strpos($line, "\n")) { return $this->write($line . "\n", strlen($line) + 1); } ++$n; return $this->write(substr($line, 0, $n), $n); } public function writeAll($string) { return $this->write($string, strlen($string)); } public function truncate($size) { return ftruncate($this->getStream(), $size); } } isFile()) { return $this->_stream = new ReadWrite($this->getPathname()); } elseif (true === $this->isDir()) { return $this->_stream = new Directory($this->getPathname()); } elseif (true === $this->isLink()) { return $this->_stream = new Link\ReadWrite($this->getPathname()); } throw new Exception('%s has an unknown type.', 0, $this->getPathname()); } public function close() { if (null === $this->_stream) { return; } return $this->_stream->close(); } public function __destruct() { $this->close(); return; } } getMode(), $createModes)) { throw new File\Exception( 'Open mode are not supported; given %d. Only %s are supported.', 0, [$this->getMode(), implode(', ', $createModes)] ); } preg_match('#^(\w+)://#', $streamName, $match); if (((isset($match[1]) && $match[1] == 'file') || !isset($match[1])) && !file_exists($streamName)) { throw new File\Exception\FileDoesNotExist( 'File %s does not exist.', 1, $streamName ); } $out = parent::_open($streamName, $context); return $out; } public function eof() { return feof($this->getStream()); } public function read($length) { if (0 > $length) { throw new File\Exception( 'Length must be greater than 0, given %d.', 2, $length ); } return fread($this->getStream(), $length); } public function readString($length) { return $this->read($length); } public function readCharacter() { return fgetc($this->getStream()); } public function readBoolean() { return (bool) $this->read(1); } public function readInteger($length = 1) { return (int) $this->read($length); } public function readFloat($length = 1) { return (float) $this->read($length); } public function readArray($format = null) { return $this->scanf($format); } public function readLine() { return fgets($this->getStream()); } public function readAll($offset = 0) { return stream_get_contents($this->getStream(), -1, $offset); } public function scanf($format) { return fscanf($this->getStream(), $format); } } getMode(), $createModes)) { throw new File\Exception( 'Open mode are not supported; given %d. Only %s are supported.', 0, [$this->getMode(), implode(', ', $createModes)] ); } preg_match('#^(\w+)://#', $streamName, $match); if (((isset($match[1]) && $match[1] == 'file') || !isset($match[1])) && !file_exists($streamName) && parent::MODE_READ_WRITE == $this->getMode()) { throw new File\Exception\FileDoesNotExist( 'File %s does not exist.', 1, $streamName ); } $out = parent::_open($streamName, $context); return $out; } public function eof() { return feof($this->getStream()); } public function read($length) { if (0 > $length) { throw new File\Exception( 'Length must be greater than 0, given %d.', 2, $length ); } return fread($this->getStream(), $length); } public function readString($length) { return $this->read($length); } public function readCharacter() { return fgetc($this->getStream()); } public function readBoolean() { return (bool) $this->read(1); } public function readInteger($length = 1) { return (int) $this->read($length); } public function readFloat($length = 1) { return (float) $this->read($length); } public function readArray($format = null) { return $this->scanf($format); } public function readLine() { return fgets($this->getStream()); } public function readAll($offset = 0) { return stream_get_contents($this->getStream(), -1, $offset); } public function scanf($format) { return fscanf($this->getStream(), $format); } public function write($string, $length) { if (0 > $length) { throw new File\Exception( 'Length must be greater than 0, given %d.', 3, $length ); } return fwrite($this->getStream(), $string, $length); } public function writeString($string) { $string = (string) $string; return $this->write($string, strlen($string)); } public function writeCharacter($char) { return $this->write((string) $char[0], 1); } public function writeBoolean($boolean) { return $this->write((string) (bool) $boolean, 1); } public function writeInteger($integer) { $integer = (string) (int) $integer; return $this->write($integer, strlen($integer)); } public function writeFloat($float) { $float = (string) (float) $float; return $this->write($float, strlen($float)); } public function writeArray(array $array) { $array = var_export($array, true); return $this->write($array, strlen($array)); } public function writeLine($line) { if (false === $n = strpos($line, "\n")) { return $this->write($line . "\n", strlen($line) + 1); } ++$n; return $this->write(substr($line, 0, $n), $n); } public function writeAll($string) { return $this->write($string, strlen($string)); } public function truncate($size) { return ftruncate($this->getStream(), $size); } } getMode(), $createModes)) { throw new File\Exception( 'Open mode are not supported; given %d. Only %s are supported.', 0, [$this->getMode(), implode(', ', $createModes)] ); } preg_match('#^(\w+)://#', $streamName, $match); if (((isset($match[1]) && $match[1] == 'file') || !isset($match[1])) && !file_exists($streamName)) { throw new File\Exception\FileDoesNotExist( 'File %s does not exist.', 1, $streamName ); } $out = parent::_open($streamName, $context); return $out; } public function write($string, $length) { if (0 > $length) { throw new File\Exception( 'Length must be greater than 0, given %d.', 2, $length ); } return fwrite($this->getStream(), $string, $length); } public function writeString($string) { $string = (string) $string; return $this->write($string, strlen($string)); } public function writeCharacter($char) { return $this->write((string) $char[0], 1); } public function writeBoolean($boolean) { return $this->write((string) (bool) $boolean, 1); } public function writeInteger($integer) { $integer = (string) (int) $integer; return $this->write($integer, strlen($integer)); } public function writeFloat($float) { $float = (string) (float) $float; return $this->write($float, strlen($float)); } public function writeArray(array $array) { $array = var_export($array, true); return $this->write($array, strlen($array)); } public function writeLine($line) { if (false === $n = strpos($line, "\n")) { return $this->write($line . "\n", strlen($line) + 1); } ++$n; return $this->write(substr($line, 0, $n), $n); } public function writeAll($string) { return $this->write($string, strlen($string)); } public function truncate($size) { return ftruncate($this->getStream(), $size); } } setListener( new Event\Listener( $this, [ 'new', 'modify', 'move' ] ) ); if (null !== $latency) { $this->setLatency($latency); } return; } public function run() { $iterator = $this->getIterator(); $previous = iterator_to_array($iterator); $current = $previous; while (true) { foreach ($current as $name => $c) { if (!isset($previous[$name])) { $this->getListener()->fire( 'new', new Event\Bucket([ 'file' => $c ]) ); continue; } if (null === $c->getHash()) { unset($current[$name]); continue; } if ($previous[$name]->getHash() != $c->getHash()) { $this->getListener()->fire( 'modify', new Event\Bucket([ 'file' => $c ]) ); } unset($previous[$name]); } foreach ($previous as $p) { $this->getListener()->fire( 'move', new Event\Bucket([ 'file' => $p ]) ); } usleep($this->getLatency() * 1000000); $previous = $current; $current = iterator_to_array($iterator); } return; } public function setLatency($latency) { $old = $this->_latency; $this->_latency = $latency; return $old; } public function getLatency() { return $this->_latency; } } getMode(), $createModes)) { throw new Exception( 'Open mode are not supported; given %d. Only %s are supported.', 0, [$this->getMode(), implode(', ', $createModes)] ); } preg_match('#^(\w+)://#', $streamName, $match); if (((isset($match[1]) && $match[1] == 'file') || !isset($match[1])) && !file_exists($streamName) && parent::MODE_TRUNCATE_WRITE == $this->getMode()) { throw new Exception\FileDoesNotExist( 'File %s does not exist.', 1, $streamName ); } $out = parent::_open($streamName, $context); return $out; } public function write($string, $length) { if (0 > $length) { throw new Exception( 'Length must be greater than 0, given %d.', 2, $length ); } return fwrite($this->getStream(), $string, $length); } public function writeString($string) { $string = (string) $string; return $this->write($string, strlen($string)); } public function writeCharacter($char) { return $this->write((string) $char[0], 1); } public function writeBoolean($boolean) { return $this->write((string) (bool) $boolean, 1); } public function writeInteger($integer) { $integer = (string) (int) $integer; return $this->write($integer, strlen($integer)); } public function writeFloat($float) { $float = (string) (float) $float; return $this->write($float, strlen($float)); } public function writeArray(array $array) { $array = var_export($array, true); return $this->write($array, strlen($array)); } public function writeLine($line) { if (false === $n = strpos($line, "\n")) { return $this->write($line . "\n", strlen($line) + 1); } ++$n; return $this->write(substr($line, 0, $n), $n); } public function writeAll($string) { return $this->write($string, strlen($string)); } public function truncate($size) { return ftruncate($this->getStream(), $size); } } _iterator = $iterator; $this->_bufferSize = max($bufferSize, 1); $this->_buffer = new \SplDoublyLinkedList(); return; } public function getInnerIterator() { return $this->_iterator; } protected function getBuffer() { return $this->_buffer; } public function getBufferSize() { return $this->_bufferSize; } public function current() { return $this->getBuffer()->current()[self::BUFFER_VALUE]; } public function key() { return $this->getBuffer()->current()[self::BUFFER_KEY]; } public function next() { $innerIterator = $this->getInnerIterator(); $buffer = $this->getBuffer(); $buffer->next(); if (false === $buffer->valid()) { for ( $bufferSize = count($buffer), $maximumBufferSize = $this->getBufferSize(); $bufferSize >= $maximumBufferSize; --$bufferSize ) { $buffer->shift(); } $innerIterator->next(); $buffer->push([ self::BUFFER_KEY => $innerIterator->key(), self::BUFFER_VALUE => $innerIterator->current() ]); $buffer->setIteratorMode($buffer::IT_MODE_LIFO | $buffer::IT_MODE_KEEP); $buffer->rewind(); $buffer->setIteratorMode($buffer::IT_MODE_FIFO | $buffer::IT_MODE_KEEP); } return; } public function previous() { $this->getBuffer()->prev(); return; } public function rewind() { $innerIterator = $this->getInnerIterator(); $buffer = $this->getBuffer(); $innerIterator->rewind(); if (true === $buffer->isEmpty()) { $buffer->push([ self::BUFFER_KEY => $innerIterator->key(), self::BUFFER_VALUE => $innerIterator->current() ]); } $buffer->rewind(); return; } public function valid() { return $this->getBuffer()->valid() && $this->getInnerIterator()->valid(); } } _callback = $callback; return; } public function current() { $handle = $this->_callback; return $this->_current = $handle($this->_key); } public function key() { return $this->_key; } public function next() { ++$this->_key; return; } public function rewind() { $this->_key = 0; $this->_current = null; return; } public function valid() { return true; } } _from = $from; $this->_to = $to; $this->_step = $step; return; } public function current() { return $this->_i; } public function key() { return $this->_key; } public function next() { ++$this->_key; $this->_i += $this->_step; return; } public function rewind() { $this->_key = 0; $this->_i = $this->_from; return; } public function valid() { return $this->_i < $this->_to; } } getIterator(); } $this->_iterator = $iterator; $this->_demuxer = $demuxer; return; } public function current() { if (null !== $this->_current) { return $this->_current; } $demuxer = $this->_demuxer; return $this->_current = $demuxer($this->_iterator->current()); } public function key() { return $this->_iterator->key(); } public function next() { $this->_current = null; return $this->_iterator->next(); } public function rewind() { return $this->_iterator->rewind(); } public function valid() { return $this->_iterator->valid(); } } _splFileInfoClass = $splFileInfoClass; parent::__construct($path); $this->setRelativePath($path); return; } public function current() { $out = parent::current(); if (null !== $this->_splFileInfoClass && $out instanceof \SplFileInfo) { $out->setInfoClass($this->_splFileInfoClass); $out = $out->getFileInfo(); if ($out instanceof \Hoa\Iterator\SplFileInfo) { $out->setRelativePath($this->getRelativePath()); } } return $out; } protected function setRelativePath($path) { $old = $this->_relativePath; $this->_relativePath = $path; return $old; } public function getRelativePath() { return $this->_relativePath; } } _splFileInfoClass = $splFileInfoClass; if (null === $flags) { parent::__construct($path); } else { parent::__construct($path, $flags); } return; } public function current() { $out = parent::current(); if (null !== $this->_splFileInfoClass && $out instanceof \SplFileInfo) { $out->setInfoClass($this->_splFileInfoClass); $out = $out->getFileInfo(); } return $out; } } _iterator = $iterator; return; } public function getInnerIterator() { return $this->_iterator; } public function current() { return $this->_current; } public function key() { return $this->_key; } public function next() { $innerIterator = $this->getInnerIterator(); $this->_valid = $innerIterator->valid(); if (false === $this->_valid) { return; } $this->_key = $innerIterator->key(); $this->_current = $innerIterator->current(); return $innerIterator->next(); } public function rewind() { $out = $this->getInnerIterator()->rewind(); $this->next(); return $out; } public function valid() { return $this->_valid; } public function hasNext() { return $this->getInnerIterator()->valid(); } public function getNext() { return $this->getInnerIterator()->current(); } public function getNextKey() { return $this->getInnerIterator()->key(); } } _iterator = $iterator; return; } public function getInnerIterator() { return $this->_iterator; } public function current() { return $this->getInnerIterator()->current(); } public function key() { return $this->getInnerIterator()->key(); } public function next() { $this->_previousKey = $this->key(); $this->_previousCurrent = $this->current(); return $this->getInnerIterator()->next(); } public function rewind() { $this->_previousKey = -1; $this->_previousCurrent = null; return $this->getInnerIterator()->rewind(); } public function valid() { return $this->getInnerIterator()->valid(); } public function hasPrevious() { return -1 !== $this->_previousKey; } public function getPrevious() { return $this->_previousCurrent; } public function getPreviousKey() { return $this->_previousKey; } } _infos[] = $default; } else { $this->_infos[$infos] = $default; } return $out; } public function current() { $out = parent::current(); foreach ($out as $key => &$value) { if (null === $value) { $value = $this->_infos[$key]; } } return $out; } } _relativePath = self::$_handlePath; self::$_handlePath = null; } else { $this->_relativePath = $path; } $this->setSplFileInfoClass($splFileInfoClass); return; } public function current() { $out = parent::current(); if (null !== $this->_splFileInfoClass && $out instanceof \SplFileInfo) { $out->setInfoClass($this->_splFileInfoClass); $out = $out->getFileInfo(); if ($out instanceof \Hoa\Iterator\SplFileInfo) { $out->setRelativePath($this->getRelativePath()); } } return $out; } public function getChildren() { self::$_handlePath = $this->getRelativePath(); $out = parent::getChildren(); if ($out instanceof \RecursiveDirectoryIterator) { $out->setSplFileInfoClass($this->_splFileInfoClass); } return $out; } public function setSplFileInfoClass($splFileInfoClass) { $this->_splFileInfoClass = $splFileInfoClass; return; } public function getRelativePath() { return $this->_relativePath; } } getIterator(); } $this->_iterator = $iterator; return; } public function current() { return $this->_iterator->current(); } public function key() { return $this->_iterator->key(); } public function next() { return $this->_iterator->next(); } public function rewind() { return $this->_iterator->rewind(); } public function valid() { return $this->_iterator->valid(); } public function getChildren() { return null; } public function hasChildren() { return false; } } hasChildren() || true === parent::accept(); } public function getChildren() { return new static( true === $this->hasChildren() ? $this->getInnerIterator()->getChildren() : null, $this->getRegex(), $this->getMode(), $this->getFlags(), $this->getPregFlags() ); } public function hasChildren() { return $this->getInnerIterator()->hasChildren(); } } _regex = $regex; $this->setMode($mode); $this->setFlags($flags); $this->setPregFlags($pregFlags); $this->replacement = null; return; } public function accept() { if (is_array(parent::current())) { return false; } $this->_key = parent::key(); $this->_current = parent::current(); $matches = []; $useKey = $this->_flags & self::USE_KEY; $subject = $useKey ? $this->_key : $this->_current; $out = false; switch ($this->_mode) { case self::MATCH: $out = 0 !== preg_match( $this->_regex, $subject, $matches, $this->_pregFlags ); break; case self::GET_MATCH: $this->_current = []; $out = 0 !== preg_match( $this->_regex, $subject, $this->_current, $this->_pregFlags ); break; case self::ALL_MATCHES: $this->_current = []; $out = 0 < preg_match_all( $this->_regex, $subject, $this->_current, $this->_pregFlags ); break; case self::SPLIT: $this->_current = preg_split( $this->_regex, $subject, null, $this->_pregFlags ); $out = is_array($this->_current) && 1 < count($this->_current); break; case self::REPLACE: $numberOfReplacement = 0; $result = preg_replace( $this->_regex, $this->replacement, $subject, -1, $numberOfReplacement ); if (null === $result || 0 === $numberOfReplacement) { $out = false; break; } if (0 !== $useKey) { $this->_key = $result; $out = true; break; } $this->_current = $result; $out = true; break; default: $out = false; break; } if (0 !== ($this->_flags & self::INVERT_MATCH)) { return false === $out; } return $out; } public function key() { return $this->_key; } public function current() { return $this->_current; } public function setMode($mode) { if ($mode < self::MATCH || $mode > self::REPLACE) { throw new \InvalidArgumentException( 'Illegal mode ' . $mode . '.' ); } $this->_mode = $mode; return; } public function setFlags($flags) { $this->_flags = $flags; return; } public function setPregFlags($pregFlags) { $this->_pregFlags = $pregFlags; return; } public function getRegex() { return $this->_regex; } public function getMode() { return $this->_mode; } public function getFlags() { return $this->_flags; } public function getPregFlags() { return $this->_pregFlags; } } = $n) { throw new Exception( 'n must be greater than 0, given %d.', 0, $n ); } if ($iterator instanceof \IteratorAggregate) { $iterator = $iterator->getIterator(); } $this->_iterator = $iterator; $this->_n = $n; $this->_body = $body; return; } public function current() { return $this->_iterator->current(); } public function key() { return $this->_iterator->key(); } public function next() { return $this->_iterator->next(); } public function rewind() { return $this->_iterator->rewind(); } public function valid() { $valid = $this->_iterator->valid(); if (true === $valid) { return true; } if (null !== $this->_body) { $handle = &$this->_body; $handle($this->_i); } $this->rewind(); if ($this->_n <= $this->_i++) { $this->_i = 1; return false; } return true; } } getMTime()) { $this->_hash = md5($this->getPathname() . $mtime); } $this->_relativePath = $relativePath; return; } public function getHash() { return $this->_hash; } public function getMTime() { try { return parent::getMTime(); } catch (\RuntimeException $e) { return -1; } } public function setRelativePath($relativePath) { $old = $this->_relativePath; $this->_relativePath = $relativePath; return $old; } public function getRelativePath() { return $this->_relativePath; } public function getRelativePathname() { if (null === $relative = $this->getRelativePath()) { return $this->getPathname(); } return substr($this->getPathname(), strlen($relative)); } } given( $counter1 = new LUT\Counter(0, 12, 3), $counter2 = new LUT\Counter(13, 23, 2), $append = new LUT\Append(), $append->append($counter1), $append->append($counter2) ) ->when($result = iterator_to_array($append, false)) ->then ->array($result) ->isEqualTo([ 0, 3, 6, 9, 13, 15, 17, 19, 21 ]); } public function case_singleton() { $this ->given( $counter1 = new LUT\Counter(0, 12, 3), $append = new LUT\Append(), $append->append($counter1) ) ->when($result = iterator_to_array($append)) ->then ->array($result) ->isEqualTo([ 0, 3, 6, 9 ]); } public function case_empty() { $this ->given($append = new LUT\Append()) ->when($result = iterator_to_array($append)) ->then ->array($result) ->isEmpty(); } } given( $innerIterator = $this->getInnerIterator(), $bufferSize = 3 ) ->when($result = new SUT($innerIterator, $bufferSize)) ->then ->object($result->getInnerIterator()) ->isIdenticalTo($innerIterator) ->integer($result->getBufferSize()) ->isEqualTo($bufferSize) ->let($buffer = $this->invoke($result)->getBuffer()) ->object($buffer) ->isInstanceOf(\SplDoublyLinkedList::class) ->boolean($buffer->isEmpty()) ->isTrue(); } public function case_negative_buffer_size() { $this ->given( $innerIterator = $this->getInnerIterator(), $bufferSize = -42 ) ->when($result = new SUT($innerIterator, $bufferSize)) ->then ->integer($result->getBufferSize()) ->isEqualTo(1); } public function case_null_buffer_size() { $this ->given( $innerIterator = $this->getInnerIterator(), $bufferSize = 0 ) ->when($result = new SUT($innerIterator, $bufferSize)) ->then ->integer($result->getBufferSize()) ->isEqualTo(1); } public function case_fast_forward() { $this ->given($iterator = new SUT($this->getInnerIterator(), 3)) ->when($result = iterator_to_array($iterator)) ->then ->array($result) ->isEqualTo(['a', 'b', 'c', 'd', 'e']) ->array(iterator_to_array($this->invoke($iterator)->getBuffer())) ->isEqualTo([ 0 => [ $iterator::BUFFER_KEY => 3, $iterator::BUFFER_VALUE => 'd' ], 1 => [ $iterator::BUFFER_KEY => 4, $iterator::BUFFER_VALUE => 'e' ], 2 => [ $iterator::BUFFER_KEY => null, $iterator::BUFFER_VALUE => null ] ]); } public function case_fast_forward_with_too_big_buffer() { $this ->given($iterator = new SUT($this->getInnerIterator(), 10)) ->when($result = iterator_to_array($iterator)) ->then ->array($result) ->isEqualTo(['a', 'b', 'c', 'd', 'e']) ->array(iterator_to_array($this->invoke($iterator)->getBuffer())) ->isEqualTo([ 0 => [ $iterator::BUFFER_KEY => 0, $iterator::BUFFER_VALUE => 'a' ], 1 => [ $iterator::BUFFER_KEY => 1, $iterator::BUFFER_VALUE => 'b' ], 2 => [ $iterator::BUFFER_KEY => 2, $iterator::BUFFER_VALUE => 'c' ], 3 => [ $iterator::BUFFER_KEY => 3, $iterator::BUFFER_VALUE => 'd' ], 4 => [ $iterator::BUFFER_KEY => 4, $iterator::BUFFER_VALUE => 'e' ], 5 => [ $iterator::BUFFER_KEY => null, $iterator::BUFFER_VALUE => null ] ]); } public function case_fast_forward_with_smallest_buffer() { $this ->given($iterator = new SUT($this->getInnerIterator(), 1)) ->when($result = iterator_to_array($iterator)) ->then ->array($result) ->isEqualTo(['a', 'b', 'c', 'd', 'e']) ->array(iterator_to_array($this->invoke($iterator)->getBuffer())) ->isEqualTo([ 0 => [ $iterator::BUFFER_KEY => null, $iterator::BUFFER_VALUE => null ] ]); } public function case_forward_forward_forward() { $this ->when($result = new SUT(new LUT\Map(['a', 'b', 'c']), 2)) ->then ->variable($result->rewind()) ->isNull() ->boolean($result->valid()) ->isTrue() ->integer($result->key()) ->isEqualTo(0) ->string($result->current()) ->isEqualTo('a') ->variable($result->next()) ->isNull() ->boolean($result->valid()) ->isTrue() ->integer($result->key()) ->isEqualTo(1) ->string($result->current()) ->isEqualTo('b') ->variable($result->next()) ->isNull() ->boolean($result->valid()) ->isTrue() ->integer($result->key()) ->isEqualTo(2) ->string($result->current()) ->isEqualTo('c') ->variable($result->next()) ->isNull() ->boolean($result->valid()) ->isFalse() ->variable($result->key()) ->isNull() ->variable($result->current()) ->isNull(); } public function case_forward_forward_backward_backward_forward_forward_forward_step_by_step() { $this ->when($result = new SUT(new LUT\Map(['a', 'b', 'c']), 3)) ->then ->variable($result->rewind()) ->isNull() ->array(iterator_to_array($this->invoke($result)->getBuffer())) ->isEqualTo([ 0 => [ $result::BUFFER_KEY => 0, $result::BUFFER_VALUE => 'a' ] ]) ->boolean($result->valid()) ->isTrue() ->integer($result->key()) ->isEqualTo(0) ->string($result->current()) ->isEqualTo('a') ->variable($result->next()) ->isNull() ->array(iterator_to_array($this->invoke($result)->getBuffer())) ->isEqualTo([ 0 => [ $result::BUFFER_KEY => 0, $result::BUFFER_VALUE => 'a' ], 1 => [ $result::BUFFER_KEY => 1, $result::BUFFER_VALUE => 'b' ] ]) ->boolean($result->valid()) ->isTrue() ->integer($result->key()) ->isEqualTo(1) ->string($result->current()) ->isEqualTo('b') ->variable($result->next()) ->isNull() ->array(iterator_to_array($this->invoke($result)->getBuffer())) ->isEqualTo([ 0 => [ $result::BUFFER_KEY => 0, $result::BUFFER_VALUE => 'a' ], 1 => [ $result::BUFFER_KEY => 1, $result::BUFFER_VALUE => 'b' ], 2 => [ $result::BUFFER_KEY => 2, $result::BUFFER_VALUE => 'c' ] ]) ->boolean($result->valid()) ->isTrue() ->integer($result->key()) ->isEqualTo(2) ->string($result->current()) ->isEqualTo('c') ->variable($result->previous()) ->isNull() ->array(iterator_to_array($this->invoke($result)->getBuffer())) ->isEqualTo([ 0 => [ $result::BUFFER_KEY => 0, $result::BUFFER_VALUE => 'a' ], 1 => [ $result::BUFFER_KEY => 1, $result::BUFFER_VALUE => 'b' ], 2 => [ $result::BUFFER_KEY => 2, $result::BUFFER_VALUE => 'c' ] ]) ->boolean($result->valid()) ->isTrue() ->integer($result->key()) ->isEqualTo(1) ->string($result->current()) ->isEqualTo('b') ->variable($result->previous()) ->isNull() ->array(iterator_to_array($this->invoke($result)->getBuffer())) ->isEqualTo([ 0 => [ $result::BUFFER_KEY => 0, $result::BUFFER_VALUE => 'a' ], 1 => [ $result::BUFFER_KEY => 1, $result::BUFFER_VALUE => 'b' ], 2 => [ $result::BUFFER_KEY => 2, $result::BUFFER_VALUE => 'c' ] ]) ->boolean($result->valid()) ->isTrue() ->integer($result->key()) ->isEqualTo(0) ->string($result->current()) ->isEqualTo('a') ->variable($result->next()) ->isNull() ->array(iterator_to_array($this->invoke($result)->getBuffer())) ->isEqualTo([ 0 => [ $result::BUFFER_KEY => 0, $result::BUFFER_VALUE => 'a' ], 1 => [ $result::BUFFER_KEY => 1, $result::BUFFER_VALUE => 'b' ], 2 => [ $result::BUFFER_KEY => 2, $result::BUFFER_VALUE => 'c' ] ]) ->boolean($result->valid()) ->isTrue() ->integer($result->key()) ->isEqualTo(1) ->string($result->current()) ->isEqualTo('b') ->variable($result->next()) ->isNull() ->array(iterator_to_array($this->invoke($result)->getBuffer())) ->isEqualTo([ 0 => [ $result::BUFFER_KEY => 0, $result::BUFFER_VALUE => 'a' ], 1 => [ $result::BUFFER_KEY => 1, $result::BUFFER_VALUE => 'b' ], 2 => [ $result::BUFFER_KEY => 2, $result::BUFFER_VALUE => 'c' ] ]) ->boolean($result->valid()) ->isTrue() ->integer($result->key()) ->isEqualTo(2) ->string($result->current()) ->isEqualTo('c') ->variable($result->next()) ->isNull() ->array(iterator_to_array($this->invoke($result)->getBuffer())) ->isEqualTo([ 0 => [ $result::BUFFER_KEY => 1, $result::BUFFER_VALUE => 'b' ], 1 => [ $result::BUFFER_KEY => 2, $result::BUFFER_VALUE => 'c' ], 2 => [ $result::BUFFER_KEY => null, $result::BUFFER_VALUE => null ] ]) ->boolean($result->valid()) ->isFalse() ->variable($result->key()) ->isNull() ->variable($result->current()) ->isNull(); } public function case_backward_out_of_buffer() { $this ->when($result = new SUT(new LUT\Map(['a', 'b', 'c']), 1)) ->then ->variable($result->rewind()) ->isNull() ->boolean($result->valid()) ->isTrue() ->integer($result->key()) ->isEqualTo(0) ->string($result->current()) ->isEqualTo('a') ->variable($result->next()) ->isNull() ->boolean($result->valid()) ->isTrue() ->integer($result->key()) ->isEqualTo(1) ->string($result->current()) ->isEqualTo('b') ->variable($result->previous()) ->isNull() ->boolean($result->valid()) ->isFalse(); } public function case_rewind_rewind() { $this ->when($result = new SUT(new LUT\Map(['a', 'b']), 3)) ->then ->variable($result->rewind()) ->isNull() ->boolean($result->valid()) ->isTrue() ->integer($result->key()) ->isEqualTo(0) ->string($result->current()) ->isEqualTo('a') ->variable($result->next()) ->isNull() ->variable($result->rewind()) ->isNull() ->boolean($result->valid()) ->isTrue() ->integer($result->key()) ->isEqualTo(0) ->string($result->current()) ->isEqualTo('a') ->variable($result->next()) ->isNull() ->array(iterator_to_array($this->invoke($result)->getBuffer())) ->isEqualTo([ 0 => [ $result::BUFFER_KEY => 0, $result::BUFFER_VALUE => 'a' ], 1 => [ $result::BUFFER_KEY => 1, $result::BUFFER_VALUE => 'b' ] ]); } protected function getInnerIterator() { return new LUT\Map(['a', 'b', 'c', 'd', 'e']); } } given( $foobar = $this->getDummyIterator(), $filter = new LUT\CallbackFilter( $foobar, function ($value) { return false === in_array($value, ['a', 'e', 'i', 'o', 'u']); } ) ) ->when($result = iterator_to_array($filter)) ->then ->array($result) ->isEqualTo([ 0 => 'f', 3 => 'b', 5 => 'r' ]); } public function case_all_callback_parameters() { $self = $this; $this ->given( $foobar = $this->getDummyIterator(), $keys = [], $values = [], $filter = new LUT\CallbackFilter( $foobar, function ($value, $key, $iterator) use ( $self, $foobar, &$keys, &$values ) { $self ->object($iterator) ->isIdenticalTo($foobar); $keys[] = $key; $values[] = $value; return false === in_array($value, ['a', 'e', 'i', 'o', 'u']); } ) ) ->when($result = iterator_to_array($filter)) ->then ->array($result) ->isEqualTo([ 0 => 'f', 3 => 'b', 5 => 'r' ]) ->array(array_combine($keys, $values)) ->isEqualTo(iterator_to_array($foobar)); } public function case_remove_all() { $this ->given( $foobar = $this->getDummyIterator(), $filter = new LUT\CallbackFilter( $foobar, function () { return false; } ) ) ->when($result = iterator_to_array($filter)) ->then ->array($result) ->isEmpty(); } public function case_remove_none() { $this ->given( $foobar = $this->getDummyIterator(), $filter = new LUT\CallbackFilter( $foobar, function () { return true; } ) ) ->when( $foobarResult = iterator_to_array($foobar), $filterResult = iterator_to_array($filter) ) ->then ->array($foobarResult) ->isEqualTo($filterResult); } public function case_recursive() { $this ->given( $foobar = $this->getDummyRecursiveIterator(), $filter = new LUT\Recursive\CallbackFilter( $foobar, function ($value) { return false === in_array($value, ['a', 'e', 'i', 'o', 'u']); } ), $iterator = new LUT\Recursive\Iterator($filter) ) ->when($result = iterator_to_array($iterator, false)) ->then ->array($result) ->isEqualTo([ 0 => 'f', 1 => 'b', 2 => 'r' ]); } public function case_recursive_remove_all() { $this ->given( $foobar = $this->getDummyRecursiveIterator(), $filter = new LUT\Recursive\CallbackFilter( $foobar, function () { return false; } ), $iterator = new LUT\Recursive\Iterator($filter) ) ->when($result = iterator_to_array($iterator)) ->then ->array($result) ->isEmpty(); } public function case_recursive_remove_none() { $this ->given( $foobar = $this->getDummyRecursiveIterator(), $filter = new LUT\Recursive\CallbackFilter( $foobar, function () { return true; } ), $foobarIterator = new LUT\Recursive\Iterator($foobar), $filterIterator = new LUT\Recursive\Iterator($filter) ) ->when( $foobarResult = iterator_to_array($foobarIterator), $filterResult = iterator_to_array($filterIterator) ) ->then ->array($foobarResult) ->isEqualTo($filterResult); } protected function getDummyIterator() { return new LUT\Map(['f', 'o', 'o', 'b', 'a', 'r']); } protected function getDummyRecursiveIterator() { return new LUT\Recursive\Map([['f', 'o', 'o'], ['b', 'a', 'r']]); } } given( $iterator = new LUT\CallbackGenerator(function ($key) { return $key * 2; }), $limit = new LUT\Limit($iterator, 0, 5) ) ->when($result = iterator_to_array($limit)) ->then ->array($result) ->isEqualTo([ 0, 2, 4, 6, 8 ]); } } given($iterator = new LUT\Counter(0, 12, 3)) ->when($result = iterator_to_array($iterator)) ->then ->array($result) ->isEqualTo([0, 3, 6, 9]); } public function case_offset() { $this ->given($iterator = new LUT\Counter(6, 12, 3)) ->when($result = iterator_to_array($iterator)) ->then ->array($result) ->isEqualTo([6, 9]); } public function case_too_small() { $this ->exception(function () { new LUT\Counter(0, 0, 0); }) ->isInstanceOf(LUT\Exception::class); } public function case_too_big() { $this ->given($iterator = new LUT\Counter(0, 12, 13)) ->when($result = iterator_to_array($iterator)) ->then ->array($result) ->isEqualTo([0]); } } given( $counter = new LUT\Counter(0, 10, 1), $multiple = new LUT\Multiple(), $multiple->attachIterator($counter), $multiple->attachIterator(clone $counter), $demultiplexer = new LUT\Demultiplexer( $multiple, function ($current) { return $current[0] * $current[1]; } ) ) ->when($result = iterator_to_array($demultiplexer, false)) ->then ->array($result) ->isEqualTo([ 0, 1, 4, 9, 16, 25, 36, 49, 64, 81 ]); } public function case_associative_keys() { $this ->given( $counter = new LUT\Counter(0, 10, 1), $multiple = new LUT\Multiple( LUT\Multiple::MIT_NEED_ANY | LUT\Multiple::MIT_KEYS_ASSOC ), $multiple->attachIterator($counter, 'one'), $multiple->attachIterator(clone $counter, 'two'), $demultiplexer = new LUT\Demultiplexer( $multiple, function ($current) { return $current['one'] * $current['two']; } ) ) ->when($result = iterator_to_array($demultiplexer, false)) ->then ->array($result) ->isEqualTo([ 0, 1, 4, 9, 16, 25, 36, 49, 64, 81 ]); } } given( $root = resolve('hoa://Test/Vfs/Root?type=directory'), resolve('hoa://Test/Vfs/Root/A?type=file'), resolve('hoa://Test/Vfs/Root/Aa?type=file'), resolve('hoa://Test/Vfs/Root/Aaa?type=file'), $iterator = new LUT\Directory($root), $result = [] ) ->when(function () use ($iterator, &$result) { foreach ($iterator as $key => $file) { $result[$key] = $file->getFilename(); $this ->object($file) ->isInstanceOf(LUT\Directory::class); } }) ->then ->array($result) ->isEqualTo([ 0 => 'A', 1 => 'Aa', 2 => 'Aaa' ]); } public function case_seek_and_dots() { $this ->given( $root = resolve('hoa://Test/Vfs/Root?type=directory'), resolve('hoa://Test/Vfs/Root/.?type=directory'), resolve('hoa://Test/Vfs/Root/..?type=directory'), resolve('hoa://Test/Vfs/Root/Skip?type=file'), resolve('hoa://Test/Vfs/Root/Gotcha?type=file'), $iterator = new LUT\Directory($root) ) ->when($result = $iterator->current()) ->then ->boolean($result->isDot()) ->isTrue() ->when( $iterator->next(), $result = $iterator->current() ) ->then ->boolean($result->isDot()) ->isTrue() ->when( $iterator->seek(3), $result = $iterator->current() ) ->then ->string($result->getFilename()) ->isEqualTo('Gotcha') ->when( $iterator->seek(2), $result = $iterator->current() ) ->then ->string($result->getFilename()) ->isEqualTo('Skip'); } public function case_recursive() { $this ->given( $root = resolve('hoa://Test/Vfs/Root?type=directory'), resolve('hoa://Test/Vfs/Root/A?type=file'), resolve('hoa://Test/Vfs/Root/Aa?type=file'), resolve('hoa://Test/Vfs/Root/Aaa?type=file'), resolve('hoa://Test/Vfs/Root/Foo?type=directory'), resolve('hoa://Test/Vfs/Root/Foo/Bar?type=directory'), resolve('hoa://Test/Vfs/Root/Foo/Bar/B?type=file'), resolve('hoa://Test/Vfs/Root/Foo/Bar/Bb?type=file'), resolve('hoa://Test/Vfs/Root/Foo/Bar/Bbb?type=file'), resolve('hoa://Test/Vfs/Root/Foo/C?type=file'), resolve('hoa://Test/Vfs/Root/Foo/Cc?type=file'), resolve('hoa://Test/Vfs/Root/Foo/Ccc?type=file'), $directory = new LUT\Recursive\Directory($root), $iterator = new LUT\Recursive\Iterator($directory), $result = [] ) ->when(function () use ($iterator, &$result) { foreach ($iterator as $file) { $result[] = $file->getFilename(); } }) ->then ->array($result) ->isEqualTo([ 'A', 'Aa', 'Aaa', 'B', 'Bb', 'Bbb', 'C', 'Cc', 'Ccc' ]); } public function case_splFileClassInfo() { $this ->given( $splFileInfo = 'Hoa\Iterator\SplFileInfo', $root = resolve('hoa://Test/Vfs/Root?type=directory'), resolve('hoa://Test/Vfs/Root/a?type=file'), resolve('hoa://Test/Vfs/Root/b?type=file'), resolve('hoa://Test/Vfs/Root/c?type=file'), resolve('hoa://Test/Vfs/Root/d?type=file'), resolve('hoa://Test/Vfs/Root/e?type=file'), resolve('hoa://Test/Vfs/Root/f?type=file'), $iterator = new LUT\Directory( $root, $splFileInfo ), $result = [] ) ->when(function () use ($iterator, $splFileInfo, &$result) { foreach ($iterator as $file) { $this ->object($file) ->isInstanceOf($splFileInfo); $result[] = $file->getFilename(); } }) ->then ->array($result) ->isEqualTo([ 'a', 'b', 'c', 'd', 'e', 'f' ]); } public function case_recursive_splFileClassInfo() { $this ->given( $splFileInfo = 'Hoa\Iterator\SplFileInfo', $root = resolve('hoa://Test/Vfs/Root?type=directory'), resolve('hoa://Test/Vfs/Root/A?type=directory'), resolve('hoa://Test/Vfs/Root/A/a?type=file'), resolve('hoa://Test/Vfs/Root/A/b?type=file'), resolve('hoa://Test/Vfs/Root/A/c?type=file'), resolve('hoa://Test/Vfs/Root/B?type=directory'), resolve('hoa://Test/Vfs/Root/B/d?type=file'), resolve('hoa://Test/Vfs/Root/B/e?type=file'), resolve('hoa://Test/Vfs/Root/B/c?type=directory'), resolve('hoa://Test/Vfs/Root/B/c/f?type=file'), $directory = new LUT\Recursive\Directory( $root, LUT\FileSystem::CURRENT_AS_FILEINFO, $splFileInfo ), $iterator = new LUT\Recursive\Iterator($directory), $result = [] ) ->when(function () use ($iterator, $splFileInfo, &$result) { foreach ($iterator as $file) { $this ->object($file) ->isInstanceOf($splFileInfo); $result[] = $file->getFilename(); } }) ->then ->array($result) ->isEqualTo([ 'a', 'b', 'c', 'd', 'e', 'f' ]); } } given( $root = resolve('hoa://Test/Vfs/Root?type=directory'), resolve('hoa://Test/Vfs/Root/.?type=directory'), resolve('hoa://Test/Vfs/Root/..?type=directory'), resolve('hoa://Test/Vfs/Root/A?type=file'), resolve('hoa://Test/Vfs/Root/B?type=file'), $iterator = new LUT\FileSystem($root), $result = [] ) ->when(function () use ($iterator, &$result) { foreach ($iterator as $pathname => $file) { $this ->object($file) ->isInstanceOf('SplFileInfo'); $result[basename($pathname)] = $file->getFilename(); } }) ->array($result) ->isEqualTo([ 'A' => 'A', 'B' => 'B' ]); } public function case_splFileClassInfo() { $this ->given( $splFileInfo = 'Hoa\Iterator\SplFileInfo', $root = resolve('hoa://Test/Vfs/Root?type=directory'), resolve('hoa://Test/Vfs/Root/a?type=file'), resolve('hoa://Test/Vfs/Root/b?type=file'), resolve('hoa://Test/Vfs/Root/c?type=file'), resolve('hoa://Test/Vfs/Root/d?type=file'), resolve('hoa://Test/Vfs/Root/e?type=file'), resolve('hoa://Test/Vfs/Root/f?type=file'), $iterator = new LUT\FileSystem( $root, LUT\FileSystem::CURRENT_AS_FILEINFO, $splFileInfo ), $result = [] ) ->when(function () use ($iterator, $splFileInfo, &$result) { foreach ($iterator as $file) { $this ->object($file) ->isInstanceOf($splFileInfo); $result[] = $file->getFilename(); } }) ->then ->array($result) ->isEqualTo([ 'a', 'b', 'c', 'd', 'e', 'f' ]); } } given( $foobar = $this->getDummyIterator(), $filter = new \Mock\Hoa\Iterator\Test\Unit\MyFilter($foobar), $this->calling($filter)->accept = function () { $value = $this->current(); return false === in_array($value, ['a', 'e', 'i', 'o', 'u']); } ) ->when($result = iterator_to_array($filter)) ->then ->array($result) ->isEqualTo([ 0 => 'f', 3 => 'b', 5 => 'r' ]); } public function case_remove_all() { $this ->given( $foobar = $this->getDummyIterator(), $filter = new \Mock\Hoa\Iterator\Test\Unit\MyFilter($foobar), $this->calling($filter)->accept = false ) ->when($result = iterator_to_array($filter)) ->then ->array($result) ->isEmpty(); } public function case_remove_none() { $this ->given( $foobar = $this->getDummyIterator(), $filter = new MyFilter($foobar) ) ->when( $foobarResult = iterator_to_array($foobar), $filterResult = iterator_to_array($filter) ) ->then ->array($foobarResult) ->isEqualTo($filterResult); } protected function getDummyIterator() { return new LUT\Map(['f', 'o', 'o', 'b', 'a', 'r']); } } given( $iterator = new LUT\Map(['a']), $infinite = new LUT\Infinite($iterator), $limit = new LUT\Limit($infinite, 0, 100) ) ->when($result = iterator_to_array($limit, false)) ->then ->array($result) ->isEqualTo(array_fill(0, 100, 'a')) ->size ->isEqualTo(100); } } given( $iterator = new LUT\Map([]), $iteratoriterator = new LUT\IteratorIterator($iterator) ) ->when($result = $iteratoriterator->getInnerIterator()) ->then ->object($result) ->isIdenticalTo($iterator); } public function case_traverse() { $this ->given( $iterator = new LUT\Map(['a', 'b', 'c']), $iteratoriterator = new LUT\IteratorIterator($iterator) ) ->when($result = iterator_to_array($iteratoriterator)) ->then ->array($result) ->isEqualTo(['a', 'b', 'c']); } public function case_recursive_leaves_only() { $this ->given( $array = [ 'a' => ['b', 'c', 'd'], 'e' => ['f', 'g', 'i'] ], $iterator = new LUT\Recursive\Map($array), $iteratoriterator = new LUT\Recursive\Iterator( $iterator, LUT\Recursive\Iterator::LEAVES_ONLY ) ) ->when($result = iterator_to_array($iteratoriterator, false)) ->then ->array($result) ->isEqualTo(['b', 'c', 'd', 'f', 'g', 'i']); } public function case_recursive_self_first() { $this ->given( $array = [ 'a' => ['b', 'c', 'd'], 'e' => ['f', 'g', 'i'] ], $iterator = new LUT\Recursive\Map($array), $iteratoriterator = new LUT\Recursive\Iterator( $iterator, LUT\Recursive\Iterator::SELF_FIRST ) ) ->when($result = iterator_to_array($iteratoriterator, false)) ->then ->array($result) ->isEqualTo([ ['b', 'c', 'd'], 'b', 'c', 'd', ['f', 'g', 'i'], 'f', 'g', 'i' ]); } public function case_recursive_child_first() { $this ->given( $array = [ 'a' => ['b', 'c', 'd'], 'e' => ['f', 'g', 'i'] ], $iterator = new LUT\Recursive\Map($array), $iteratoriterator = new LUT\Recursive\Iterator( $iterator, LUT\Recursive\Iterator::CHILD_FIRST ) ) ->when($result = iterator_to_array($iteratoriterator, false)) ->then ->array($result) ->isEqualTo([ 'b', 'c', 'd', ['b', 'c', 'd'], 'f', 'g', 'i', ['f', 'g', 'i'] ]); } } given( $iterator = new LUT\Map(self::$_dummyArray), $limit = new LUT\Limit($iterator, 2, 3) ) ->when($result = iterator_to_array($limit)) ->then ->array($result) ->isEqualTo([ 2 => 'o', 3 => 'b', 4 => 'a' ]); } public function case_negative_offset() { $this ->given($iterator = new LUT\Map(self::$_dummyArray)) ->exception(function () use ($iterator) { new LUT\Limit($iterator, -2, 3); }) ->isInstanceOf(\OutOfRangeException::class); } public function case_empty() { $this ->given( $iterator = new LUT\Map(self::$_dummyArray), $limit = new LUT\Limit($iterator, 0, 0) ) ->exception(function () use ($limit) { iterator_to_array($limit); }) ->isInstanceOf(\OutOfBoundsException::class); } } given( $iterator = new LUT\Map(['a', 'b', 'c']), $lookahead = new LUT\Lookahead($iterator) ) ->when($result = iterator_to_array($iterator)) ->then ->array($result) ->isEqualTo(['a', 'b', 'c']); } public function case_check_ahead() { $this ->given( $iterator = new LUT\Map(['a', 'b', 'c']), $lookahead = new LUT\Lookahead($iterator) ) ->when( $lookahead->rewind(), $key = $lookahead->key(), $current = $lookahead->current(), $hasNext = $lookahead->hasNext(), $next = $lookahead->getNext() ) ->then ->integer($key) ->isEqualTo(0) ->string($current) ->isEqualTo('a') ->boolean($hasNext) ->isTrue() ->string($next) ->isEqualTo('b') ->when( $lookahead->next(), $key = $lookahead->key(), $current = $lookahead->current(), $hasNext = $lookahead->hasNext(), $next = $lookahead->getNext() ) ->then ->integer($key) ->isEqualTo(1) ->string($current) ->isEqualTo('b') ->boolean($hasNext) ->isTrue() ->string($next) ->isEqualTo('c') ->when( $lookahead->next(), $key = $lookahead->key(), $current = $lookahead->current(), $hasNext = $lookahead->hasNext(), $next = $lookahead->getNext() ) ->then ->integer($key) ->isEqualTo(2) ->string($current) ->isEqualTo('c') ->boolean($hasNext) ->isFalse() ->variable($next) ->isNull(); } public function case_double_rewind() { $this ->given( $iterator = new LUT\Map(['a', 'b', 'c']), $lookahead = new LUT\Lookahead($iterator) ) ->when( $lookahead->rewind(), $key = $lookahead->key(), $current = $lookahead->current(), $hasNext = $lookahead->hasNext(), $next = $lookahead->getNext() ) ->then ->integer($key) ->isEqualTo(0) ->string($current) ->isEqualTo('a') ->boolean($hasNext) ->isTrue() ->string($next) ->isEqualTo('b') ->when( $lookahead->rewind(), $key = $lookahead->key(), $current = $lookahead->current(), $hasNext = $lookahead->hasNext(), $next = $lookahead->getNext() ) ->then ->integer($key) ->isEqualTo(0) ->string($current) ->isEqualTo('a') ->boolean($hasNext) ->isTrue() ->string($next) ->isEqualTo('b'); } public function case_empty() { $this ->given( $iterator = new LUT\Mock(), $lookahead = new LUT\Lookahead($iterator) ) ->when( $lookahead->rewind(), $valid = $lookahead->valid() ) ->then ->boolean($valid) ->isFalse(); } } given( $iterator = new LUT\Map(['a', 'b', 'c']), $lookbehind = new LUT\Lookbehind($iterator) ) ->when($result = iterator_to_array($iterator)) ->then ->array($result) ->isEqualTo(['a', 'b', 'c']); } public function case_check_behind() { $this ->given( $iterator = new LUT\Map(['a', 'b', 'c']), $lookbehind = new LUT\Lookbehind($iterator) ) ->when( $lookbehind->rewind(), $key = $lookbehind->key(), $current = $lookbehind->current(), $hasPrevious = $lookbehind->hasPrevious(), $previous = $lookbehind->getPrevious() ) ->then ->integer($key) ->isEqualTo(0) ->string($current) ->isEqualTo('a') ->boolean($hasPrevious) ->isFalse() ->variable($previous) ->isNull() ->when( $lookbehind->next(), $key = $lookbehind->key(), $current = $lookbehind->current(), $hasPrevious = $lookbehind->hasPrevious(), $previous = $lookbehind->getPrevious() ) ->then ->integer($key) ->isEqualTo(1) ->string($current) ->isEqualTo('b') ->boolean($hasPrevious) ->isTrue() ->string($previous) ->isEqualTo('a') ->when( $lookbehind->next(), $key = $lookbehind->key(), $current = $lookbehind->current(), $hasPrevious = $lookbehind->hasPrevious(), $previous = $lookbehind->getPrevious() ) ->then ->integer($key) ->isEqualTo(2) ->string($current) ->isEqualTo('c') ->boolean($hasPrevious) ->isTrue() ->string($previous) ->isEqualTo('b'); } public function case_double_rewind() { $this ->given( $iterator = new LUT\Map(['a', 'b', 'c']), $lookbehind = new LUT\Lookbehind($iterator) ) ->when( $lookbehind->rewind(), $key = $lookbehind->key(), $current = $lookbehind->current(), $hasPrevious = $lookbehind->hasPrevious() ) ->then ->integer($key) ->isEqualTo(0) ->string($current) ->isEqualTo('a') ->boolean($hasPrevious) ->isFalse() ->when( $lookbehind->rewind(), $key = $lookbehind->key(), $current = $lookbehind->current(), $hasPrevious = $lookbehind->hasPrevious() ) ->then ->integer($key) ->isEqualTo(0) ->string($current) ->isEqualTo('a') ->boolean($hasPrevious) ->isFalse(); } public function case_empty() { $this ->given( $iterator = new LUT\Mock(), $lookbehind = new LUT\Lookbehind($iterator) ) ->when( $lookbehind->rewind(), $valid = $lookbehind->valid() ) ->then ->boolean($valid) ->isFalse(); } } given($iterator = new LUT\Map(self::$_dummyArray)) ->when($result = iterator_to_array($iterator)) ->then ->array($result) ->isEqualTo(self::$_dummyArray); } public function case_empty() { $this ->given($iterator = new LUT\Map()) ->when($result = iterator_to_array($iterator)) ->then ->array($result) ->isEmpty(); } public function case_recursive() { $this ->given( $array = [ 'a' => ['b', 'c', 'd'], 'e' => ['f', 'g', 'i'], 'k' ], $iterator = new LUT\Recursive\Map($array) ) ->when(function () use ($iterator) { foreach ($iterator as $key => $value) { if ('a' === $key) { $this ->boolean($iterator->hasChildren()) ->isTrue() ->object($children = $iterator->getChildren()) ->isInstanceOf(LUT\Recursive\Map::class) ->array(iterator_to_array($children)) ->isEqualTo(['b', 'c', 'd']); } elseif ('e' === $key) { $this ->boolean($iterator->hasChildren()) ->isTrue() ->object($children = $iterator->getChildren()) ->isInstanceOf(LUT\Recursive\Map::class) ->array(iterator_to_array($children)) ->isEqualTo(['f', 'g', 'i']); } elseif ('k' === $value) { $this ->boolean($iterator->hasChildren()) ->isFalse(); } } }); } } given($iterator = new LUT\Mock()) ->when($result = iterator_to_array($iterator)) ->then ->array($result) ->isEmpty(); } public function case_recursive_mock_mock() { $this ->when($iterator = new LUT\Recursive\Mock(new LUT\Mock())) ->then ->variable($iterator->getChildren()) ->isNull() ->boolean($iterator->hasChildren()) ->isFalse(); } public function case_recursive() { $this ->given( $map = new LUT\Map(['a', 'b', 'c']), $mock = new LUT\Recursive\Mock($map), $iteratoriterator = new LUT\Recursive\Iterator($mock) ) ->when($result = iterator_to_array($map, false)) ->then ->array($result) ->isEqualTo(['a', 'b', 'c']); } } given( $foo = new LUT\Map(['f', 'o', 'o']), $bar = new LUT\Map(['b', 'a', 'r']), $multiple = new LUT\Multiple( LUT\Multiple::MIT_NEED_ANY | LUT\Multiple::MIT_KEYS_ASSOC ), $multiple->attachIterator($foo, 'one'), $multiple->attachIterator($bar, 'two') ) ->when($result = iterator_to_array($multiple, false)) ->then ->array($result) ->isEqualTo([ ['one' => 'f', 'two' => 'b'], ['one' => 'o', 'two' => 'a'], ['one' => 'o', 'two' => 'r'] ]); } public function case_default_value() { $this ->given( $foobar = new LUT\Map(['f', 'o', 'o', 'b', 'a', 'r']), $baz = new LUT\Map(['b', 'a', 'z']), $multiple = new LUT\Multiple( LUT\Multiple::MIT_NEED_ANY | LUT\Multiple::MIT_KEYS_ASSOC ), $multiple->attachIterator($foobar, 'one', '!'), $multiple->attachIterator($baz, 'two', '?') ) ->when($result = iterator_to_array($multiple, false)) ->then ->array($result) ->isEqualTo([ ['one' => 'f', 'two' => 'b'], ['one' => 'o', 'two' => 'a'], ['one' => 'o', 'two' => 'z'], ['one' => 'b', 'two' => '?'], ['one' => 'a', 'two' => '?'], ['one' => 'r', 'two' => '?'], ]); } public function case_empty() { $this ->given($multiple = new LUT\Multiple()) ->when($result = iterator_to_array($multiple)) ->then ->array($result) ->isEmpty(); } } given( $dummyArray = ['f', 'o', 'o', 'b', 'a', 'r'], $iterator = new LUT\Map($dummyArray), $norewind = new LUT\NoRewind($iterator) ) ->when($result = iterator_to_array($norewind)) ->then ->array($result) ->isEqualTo($dummyArray) ->when($norewind->rewind()) ->boolean($norewind->valid()) ->isFalse() ->when($result = iterator_to_array($norewind)) ->then ->array($result) ->isEmpty(); } } given( $map = new LUT\Map([ 'abc', 'dea', 'fgh', 'iaj', 'klm' ]), $iterator = new LUT\RegularExpression($map, '/a/') ) ->when($result = iterator_to_array($iterator)) ->then ->array($result) ->isEqualTo([ 0 => 'abc', 1 => 'dea', 3 => 'iaj' ]); } public function case_recursive() { $this ->given( $map = new LUT\Recursive\Map([ ['abc', 'dea', 'fgh'], ['iaj', 'klm'] ]), $regex = new LUT\Recursive\RegularExpression($map, '/a/'), $iterator = new LUT\Recursive\Iterator($regex) ) ->when($result = iterator_to_array($iterator, false)) ->then ->array($result) ->isEqualTo([ 0 => 'abc', 1 => 'dea', 2 => 'iaj' ]); } public function case_recursive_children_flags() { $this ->given( $map = new LUT\Recursive\Map([ ['abc', 'dea', 'fgh'], ['iaj', 'klm'] ]), $mode = LUT\Recursive\RegularExpression::ALL_MATCHES, $flag = LUT\Recursive\RegularExpression::USE_KEY, $pregFlag = LUT\Recursive\RegularExpression::ALL_MATCHES, $iterator = new LUT\Recursive\RegularExpression( $map, '/a/', $mode, $flag, $pregFlag ) ) ->when($result = $iterator->getChildren()) ->then ->object($result) ->isInstanceOf(LUT\Recursive\RegularExpression::class) ->integer($result->getMode()) ->isEqualTo($mode) ->integer($result->getFlags()) ->isEqualTo($flag) ->integer($result->getPregFlags()) ->isEqualTo($pregFlag); } } given( $iterator = new LUT\Map(self::$_dummyArray), $repeater = new LUT\Repeater($iterator, 3) ) ->when($result = iterator_to_array($repeater)) ->then ->array($result) ->isEqualTo( self::$_dummyArray + self::$_dummyArray + self::$_dummyArray ); } public function case_with_body() { $self = $this; $this ->given( $iterator = new LUT\Map(self::$_dummyArray), $count = 0, $repeater = new LUT\Repeater( $iterator, 3, function ($repetition) use ($self, &$count) { $this ->integer($repetition) ->isEqualTo($count + 1); ++$count; }) ) ->when($result = iterator_to_array($repeater)) ->then ->array($result) ->isEqualTo( self::$_dummyArray + self::$_dummyArray + self::$_dummyArray ) ->integer($count) ->isEqualTo(3); } } given($pathname = 'hoa://Test/Vfs/Foo.bar?type=file') ->when($result = new LUT\SplFileInfo($pathname)) ->then ->boolean($result->isFile()) ->isTrue() ->string($result->getType()) ->isEqualTo('file'); } public function case_directory() { $this ->given($pathname = 'hoa://Test/Vfs/Foo?type=directory') ->when($result = new LUT\SplFileInfo($pathname)) ->then ->boolean($result->isDir()) ->isTrue() ->string($result->getType()) ->isEqualTo('dir'); } public function case_path_informations() { $this ->given( $relativePath = 'hoa://Test/Vfs/A/B/', $relativePathname = 'C/Foo.bar', $pathname = $relativePath . $relativePathname ) ->when($result = new LUT\SplFileInfo($pathname . '?type=file', $relativePath)) ->then ->boolean($result->isFile()) ->isTrue() ->string($result->getBasename()) ->isEqualTo('Foo.bar?type=file') ->string($result->getExtension()) ->isEqualTo('bar?type=file') ->string($result->getRelativePath()) ->isEqualTo($relativePath) ->string($result->getRelativePathname()) ->isEqualTo($relativePathname . '?type=file') ->string($result->getPath()) ->isEqualTo('hoa://Test/Vfs/A/B/C') ->string($result->getPathname()) ->isEqualTo($pathname . '?type=file'); } public function case_times() { $this ->given( $timestamp = $this->realdom->boundinteger( $this->realdom->timestamp('yesterday'), $this->realdom->timestamp('tomorrow') ), $atime = $this->sample($timestamp), $ctime = $this->sample($timestamp), $mtime = $this->sample($timestamp), $pathname = 'hoa://Test/Vfs/Foo.bar?' . http_build_query([ 'type' => 'file', 'atime' => $atime, 'ctime' => $ctime, 'mtime' => $mtime ]) ) ->when($result = new LUT\SplFileInfo($pathname)) ->then ->integer($result->getATime()) ->isEqualTo($atime) ->integer($result->getCTime()) ->isEqualTo($ctime) ->integer($result->getMTime()) ->isEqualTo($mtime); } public function case_permissions() { $this ->given($pathname = 'hoa://Test/Vfs/Fo.bar?type=file&permissions=0744') ->when($result = new LUT\SplFileInfo($pathname)) ->then ->boolean($result->isReadable()) ->isTrue() ->boolean($result->isWritable()) ->isTrue() ->boolean($result->isExecutable()) ->isTrue() ->given($pathname = 'hoa://Test/Vfs/Foo.bar?type=file&permissions=0644') ->when($result = new LUT\SplFileInfo($pathname)) ->then ->boolean($result->isReadable()) ->isTrue() ->boolean($result->isWritable()) ->isTrue() ->boolean($result->isExecutable()) ->isFalse() ->given($pathname = 'hoa://Test/Vfs/Fooo.bar?type=file&permissions=0444') ->when($result = new LUT\SplFileInfo($pathname)) ->then ->boolean($result->isReadable()) ->isTrue() ->boolean($result->isWritable()) ->isFalse() ->boolean($result->isExecutable()) ->isFalse() ->given($pathname = 'hoa://Test/Vfs/Foooo.bar?type=file&permissions=0044') ->when($result = new LUT\SplFileInfo($pathname)) ->then ->boolean($result->isReadable()) ->isFalse() ->boolean($result->isWritable()) ->isFalse() ->boolean($result->isExecutable()) ->isFalse(); } } getOption($v)) { switch ($c) { case 'E': $exists = false; break; case 'u': $unfold = true; break; case 't': $tree = true; break; case 'V': $verbose = false; break; case 'h': case '?': return $this->usage(); case '__ambiguous': $this->resolveOptionAmbiguity($v); break; } } $this->parser->listInputs($path); if (null === $path) { return $this->usage(); } if (true === $tree) { $protocol = Protocol::getInstance(); $foo = substr($path, 0, 6); if ('hoa://' !== $foo) { return; } $path = substr($path, 6); $current = $protocol; foreach (explode('/', $path) as $component) { if (!isset($current[$component])) { break; } $current = $current[$component]; } echo $current; return; } if (true === $verbose) { echo Console\Cursor::colorize('foreground(yellow)'), $path, Console\Cursor::colorize('normal'), ' is equivalent to:', "\n"; } $resolved = resolve($path, $exists, $unfold); foreach ((array) $resolved as $r) { echo $r, "\n"; } return; } public function usage() { echo 'Usage : protocol:resolve path', "\n", 'Options :', "\n", $this->makeUsageOptionsList([ 'E' => 'Do not check if the resolution result exists.', 'u' => 'Unfold all possible results.', 't' => 'Print the tree from the path.', 'V' => 'No-verbose, i.e. be as quiet as possible, just print ' . 'essential information.', 'help' => 'This help.' ]), "\n"; return; } } __halt_compiler(); Resolve `hoa://` paths. _reach) as $part) { $out[] = "\r" . $part . strtolower($head) . $queue; } $out[] = "\r" . dirname(dirname(dirname(dirname(__DIR__)))) . $queue; return implode(RS, $out); } $out = []; foreach (explode(RS, $this->_reach) as $part) { $pos = strrpos(rtrim($part, DIRECTORY_SEPARATOR), DIRECTORY_SEPARATOR) + 1; $head = substr($part, 0, $pos); $tail = substr($part, $pos); $out[] = $head . strtolower($tail); } $this->_reach = implode(RS, $out); return parent::reach($queue); } } _name = $name; } if (null !== $reach) { $this->_reach = $reach; } foreach ($children as $child) { $this[] = $child; } return; } public function offsetSet($name, $node) { if (!($node instanceof self)) { throw new Protocol\Exception( 'Protocol node must extend %s.', 0, __CLASS__ ); } if (empty($name)) { $name = $node->getName(); } if (empty($name)) { throw new Protocol\Exception( 'Cannot add a node to the `hoa://` protocol without a name.', 1 ); } $this->_children[$name] = $node; return; } public function offsetGet($name) { if (!isset($this[$name])) { throw new Protocol\Exception( 'Node %s does not exist.', 2, $name ); } return $this->_children[$name]; } public function offsetExists($name) { return true === array_key_exists($name, $this->_children); } public function offsetUnset($name) { unset($this->_children[$name]); return; } protected function _resolve($path, &$accumulator, $id = null) { if (substr($path, 0, 6) == 'hoa://') { $path = substr($path, 6); } if (empty($path)) { return null; } if (null === $accumulator) { $accumulator = []; $posId = strpos($path, '#'); if (false !== $posId) { $id = substr($path, $posId + 1); $path = substr($path, 0, $posId); } else { $id = null; } } $path = trim($path, '/'); $pos = strpos($path, '/'); if (false !== $pos) { $next = substr($path, 0, $pos); } else { $next = $path; } if (isset($this[$next])) { if (false === $pos) { if (null === $id) { $this->_resolveChoice($this[$next]->reach(), $accumulator); return true; } $accumulator = null; return $this[$next]->reachId($id); } $tnext = $this[$next]; $this->_resolveChoice($tnext->reach(), $accumulator); return $tnext->_resolve(substr($path, $pos + 1), $accumulator, $id); } $this->_resolveChoice($this->reach($path), $accumulator); return true; } protected function _resolveChoice($reach, array &$accumulator) { if (empty($accumulator)) { $accumulator = explode(RS, $reach); return; } if (false === strpos($reach, RS)) { if (false !== $pos = strrpos($reach, "\r")) { $reach = substr($reach, $pos + 1); foreach ($accumulator as &$entry) { $entry = null; } } foreach ($accumulator as &$entry) { $entry .= $reach; } return; } $choices = explode(RS, $reach); $ref = $accumulator; $accumulator = []; foreach ($choices as $choice) { if (false !== $pos = strrpos($choice, "\r")) { $choice = substr($choice, $pos + 1); foreach ($ref as $entry) { $accumulator[] = $choice; } } else { foreach ($ref as $entry) { $accumulator[] = $entry . $choice; } } } unset($ref); return; } public function reach($queue = null) { return empty($queue) ? $this->_reach : $queue; } public function reachId($id) { throw new Protocol\Exception( 'The node %s has no ID support (tried to reach #%s).', 4, [$this->getName(), $id] ); } public function setReach($reach) { $old = $this->_reach; $this->_reach = $reach; return $old; } public function getName() { return $this->_name; } protected function getReach() { return $this->_reach; } public function getIterator() { return new \ArrayIterator($this->_children); } public static function getRoot() { return Protocol::getInstance(); } public function __toString() { static $i = 0; $out = str_repeat(' ', $i) . $this->getName() . "\n"; foreach ($this as $node) { ++$i; $out .= $node; --$i; } return $out; } } Consistency::flexEntity('Hoa\Protocol\Node\Node'); initialize(); return; } public static function getInstance() { if (null === static::$_instance) { static::$_instance = new static(); } return static::$_instance; } protected function initialize() { $root = dirname(dirname(__DIR__)); $cwd = 'cli' === PHP_SAPI ? dirname(realpath($_SERVER['argv'][0])) : getcwd(); $this[] = new Node( 'Application', $cwd . DS, [ new Node('Public', 'Public' . DS) ] ); $this[] = new Node( 'Data', dirname($cwd) . DS, [ new Node( 'Etc', 'Etc' . DS, [ new Node('Configuration', 'Configuration' . DS), new Node('Locale', 'Locale' . DS) ] ), new Node('Lost+found', 'Lost+found' . DS), new Node('Temporary', 'Temporary' . DS), new Node( 'Variable', 'Variable' . DS, [ new Node('Cache', 'Cache' . DS), new Node('Database', 'Database' . DS), new Node('Log', 'Log' . DS), new Node('Private', 'Private' . DS), new Node('Run', 'Run' . DS), new Node('Test', 'Test' . DS) ] ) ] ); $this[] = new Node\Library( 'Library', $root . DS . 'Hoathis' . DS . RS . $root . DS . 'Hoa' . DS ); return; } public function resolve($path, $exists = true, $unfold = false) { if (substr($path, 0, 6) !== 'hoa://') { if (true === is_dir($path)) { $path = rtrim($path, '/\\'); if (0 === strlen($path)) { $path = '/'; } } return $path; } if (isset(self::$_cache[$path])) { $handle = self::$_cache[$path]; } else { $out = $this->_resolve($path, $handle); if (!is_array($handle)) { return $out; } $handle = array_values(array_unique($handle, SORT_REGULAR)); foreach ($handle as &$entry) { if (true === is_dir($entry)) { $entry = rtrim($entry, '/\\'); if (0 === strlen($entry)) { $entry = '/'; } } } self::$_cache[$path] = $handle; } if (true === $unfold) { if (true !== $exists) { return $handle; } $out = []; foreach ($handle as $solution) { if (file_exists($solution)) { $out[] = $solution; } } return $out; } if (true !== $exists) { return $handle[0]; } foreach ($handle as $solution) { if (file_exists($solution)) { return $solution; } } return static::NO_RESOLUTION; } public static function clearCache() { self::$_cache = []; return; } } Consistency::flexEntity('Hoa\Protocol\Protocol'); when($result = new SUT('foo', 0)) ->then ->object($result) ->isInstanceOf('Hoa\Exception\Exception'); } } given( $this->constant->WITH_COMPOSER = false, $node = new SUT('foo', 'bar') ) ->when($result = $node->reach()) ->then ->string($result) ->isEqualTo('bar'); } public function case_reach_without_composer_with_a_queue() { $this ->given( $this->constant->WITH_COMPOSER = false, $node = new SUT('foo', 'bar') ) ->when($result = $node->reach('baz')) ->then ->string($result) ->isEqualTo('baz'); } public function case_reach_with_composer_without_a_queue_and_a_single_reach() { $this ->given( $this->constant->WITH_COMPOSER = true, $node = new SUT('foo', 'Bar' . DS . 'Baz' . DS . 'Qux' . DS) ) ->when($result = $node->reach()) ->then ->string($result) ->isEqualTo('Bar' . DS . 'Baz' . DS . 'qux' . DS); } public function case_reach_with_composer_without_a_queue_and_a_multiple_reaches() { $this ->given( $this->constant->WITH_COMPOSER = true, $node = new SUT( 'foo', 'Bar' . DS . 'Baz' . DS . 'Qux' . DS . RS . 'Hello' . DS . 'Mister' . DS . 'Anderson' . DS ) ) ->when($result = $node->reach()) ->then ->string($result) ->isEqualTo( 'Bar' . DS . 'Baz' . DS . 'qux' . DS . RS . 'Hello' . DS . 'Mister' . DS . 'anderson' . DS ); } public function case_reach_with_composer_with_a_simple_queue() { $this ->given( $this->constant->WITH_COMPOSER = true, $node = new SUT('foo', 'Bar' . DS . 'Baz' . DS . 'Qux' . DS) ) ->when($result = $node->reach('Hello')) ->then ->string($result) ->isEqualTo( "\r" . 'Bar' . DS . 'Baz' . DS . 'Qux' . DS . 'hello' . RS . "\r" . dirname(dirname(dirname(dirname(dirname(dirname(__DIR__)))))) ); } public function case_reach_with_composer_with_a_queue() { $this ->given( $this->constant->WITH_COMPOSER = true, $node = new SUT('foo', 'Bar' . DS) ) ->when($result = $node->reach('Hello/Mister/Anderson')) ->then ->string($result) ->isEqualTo( "\r" . 'Bar' . DS . 'hello' . DS . 'Mister' . DS . 'Anderson' . RS . "\r" . dirname(dirname(dirname(dirname(dirname(dirname(__DIR__)))))) . DS . 'Mister' . DS . 'Anderson' ); } } when($result = new SUT()) ->then ->object($result) ->isInstanceOf('ArrayAccess') ->isInstanceOf('IteratorAggregate'); } public function case_empty_constructor() { $this ->when($result = new SUT()) ->then ->variable($result->getName()) ->isNull() ->array(iterator_to_array($result->getIterator())) ->isEmpty(); } public function case_constructor_with_a_name() { $this ->given($name = 'foo') ->when($result = new SUT($name)) ->then ->string($result->getName()) ->isEqualTo($name) ->array(iterator_to_array($result->getIterator())) ->isEmpty(); } public function case_constructor_with_a_name_and_children() { $this ->given( $name = 'foo', $children = [new SUT('bar'), new SUT('baz')] ) ->when($result = new SUT($name, '', $children)) ->then ->string($result->getName()) ->isEqualTo($name) ->array(iterator_to_array($result->getIterator())) ->hasSize(2); } public function case_offset_set() { $this ->given( $root = new SUT(), $name = 'foo', $node = new SUT(), $oldCountChildren = count(iterator_to_array($root->getIterator())) ) ->when($result = $root->offsetSet($name, $node)) ->then ->integer(count(iterator_to_array($root->getIterator()))) ->isEqualTo($oldCountChildren + 1) ->object($root[$name]) ->isIdenticalTo($node); } public function case_offset_set_not_a_node() { $this ->given($root = new SUT()) ->exception(function () use ($root) { $root->offsetSet('foo', null); }) ->isInstanceOf('Hoa\Protocol\Exception'); } public function case_offset_set_no_name() { $this ->given($root = new SUT()) ->exception(function () use ($root) { $root->offsetSet(null, new SUT()); }) ->isInstanceOf('Hoa\Protocol\Exception'); } public function case_offset_get() { $this ->given( $root = new SUT(), $child = new SUT(), $root['foo'] = $child ) ->when($result = $root->offsetGet('foo')) ->then ->object($result) ->isIdenticalTo($child); } public function case_offset_get_an_unknown_name() { $this ->given($root = new SUT()) ->exception(function () use ($root) { $root->offsetGet('foo'); }) ->isInstanceOf('Hoa\Protocol\Exception'); } public function case_offset_exists() { $this ->given( $root = new SUT(), $child = new SUT(), $root['foo'] = $child ) ->when($result = $root->offsetExists('foo')) ->then ->boolean($result) ->isTrue(); } public function case_offset_not_exists() { $this ->given($root = new SUT()) ->when($result = $root->offsetExists('foo')) ->then ->boolean($result) ->isFalse(); } public function case_offset_unset() { $this ->given( $root = new SUT(), $child = new SUT(), $root['foo'] = $child ) ->when($result = $root->offsetUnset('foo')) ->then ->boolean($root->offsetExists('foo')) ->isFalse(); } public function case_reach() { $this ->given( $reach = 'bar', $node = new SUT('foo', $reach) ) ->when($result = $node->reach()) ->then ->string($result) ->isEqualTo($reach); } public function case_reach_with_a_queue() { $this ->given( $queue = 'baz', $node = new SUT('foo', 'bar') ) ->when($result = $node->reach('baz')) ->then ->string($result) ->isEqualTo($queue); } public function case_reach_id() { $this ->given($node = new SUT()) ->exception(function () use ($node) { $node->reachId('foo'); }) ->isInstanceOf('Hoa\Protocol\Exception'); } public function case_set_reach() { $this ->given( $reach = 'bar', $node = new SUT('foo', $reach) ) ->when($result = $node->setReach('baz')) ->then ->string($result) ->isEqualTo($reach) ->string($node->reach()) ->isEqualTo('baz'); } public function case_get_name() { $this ->given( $name = 'foo', $node = new SUT($name) ) ->when($result = $node->getName()) ->then ->string($result) ->isEqualTo($name); } public function case_get_iterator() { $this ->given( $childA = new SUT('bar'), $childB = new SUT('baz'), $children = [$childA, $childB] ) ->when($result = new SUT('foo', '', $children)) ->then ->object($result->getIterator()) ->isInstanceOf('ArrayIterator') ->array(iterator_to_array($result->getIterator())) ->isEqualTo([ 'bar' => $childA, 'baz' => $childB ]); } public function case_get_root() { $this ->when($result = SUT::getRoot()) ->then ->object($result) ->isIdenticalTo(LUT::getInstance()); } public function case_to_string_as_leaf() { $this ->given($node = new SUT('foo')) ->when($result = $node->__toString()) ->then ->string($result) ->isEqualTo('foo' . "\n"); } public function case_to_string_as_node() { $this ->given( $node = new SUT('foo'), $node[] = new SUT('bar'), $node[] = new SUT('baz') ) ->when($result = $node->__toString()) ->then ->string($result) ->isEqualTo( 'foo' . "\n" . ' bar' . "\n" . ' baz' . "\n" ); } } when($result = SUT::getInstance()) ->then ->object($result) ->isInstanceOf('Hoa\Protocol\Node'); } public function case_default_tree() { $this ->when($result = SUT::getInstance()) ->then ->object($result['Application'])->isInstanceOf('Hoa\Protocol\Node\Node') ->object($result['Application']['Public'])->isInstanceOf('Hoa\Protocol\Node\Node') ->object($result['Data'])->isInstanceOf('Hoa\Protocol\Node\Node') ->object($result['Data']['Etc'])->isInstanceOf('Hoa\Protocol\Node\Node') ->object($result['Data']['Etc']['Configuration'])->isInstanceOf('Hoa\Protocol\Node\Node') ->object($result['Data']['Etc']['Locale'])->isInstanceOf('Hoa\Protocol\Node\Node') ->object($result['Data']['Lost+found'])->isInstanceOf('Hoa\Protocol\Node\Node') ->object($result['Data']['Temporary'])->isInstanceOf('Hoa\Protocol\Node\Node') ->object($result['Data']['Variable'])->isInstanceOf('Hoa\Protocol\Node\Node') ->object($result['Data']['Variable']['Cache'])->isInstanceOf('Hoa\Protocol\Node\Node') ->object($result['Data']['Variable']['Database'])->isInstanceOf('Hoa\Protocol\Node\Node') ->object($result['Data']['Variable']['Log'])->isInstanceOf('Hoa\Protocol\Node\Node') ->object($result['Data']['Variable']['Private'])->isInstanceOf('Hoa\Protocol\Node\Node') ->object($result['Data']['Variable']['Run'])->isInstanceOf('Hoa\Protocol\Node\Node') ->object($result['Data']['Variable']['Test'])->isInstanceOf('Hoa\Protocol\Node\Node') ->object($result['Library'])->isInstanceOf('Hoa\Protocol\Node\Library') ->string($result['Library']->reach()) ->isEqualTo( dirname(dirname(dirname(dirname(__DIR__)))) . DS . 'hoathis' . DS . RS . dirname(dirname(dirname(dirname(__DIR__)))) . DS . 'hoa' . DS ); } public function case_resolve_not_a_hoa_path() { $this ->given($protocol = SUT::getInstance()) ->when($result = $protocol->resolve('/foo/bar')) ->then ->string($result) ->isEqualTo('/foo/bar'); } public function case_resolve_to_non_existing_resource() { $this ->given($protocol = SUT::getInstance()) ->when($result = $protocol->resolve('hoa://Application/Foo/Bar')) ->then ->string($result) ->isEqualTo(SUT::NO_RESOLUTION); } public function case_resolve_does_not_test_if_exists() { $this ->given($protocol = SUT::getInstance()) ->when($result = $protocol->resolve('hoa://Application/Foo/Bar', false)) ->then ->string($result) ->isEqualTo('/Foo/Bar'); } public function case_resolve_unfold_to_existing_resources() { $this ->given($protocol = SUT::getInstance()) ->when($result = $protocol->resolve('hoa://Library', true, true)) ->then ->array($result) ->contains( dirname(dirname(dirname(dirname(__DIR__)))) . DS . 'hoa' ); } public function case_resolve_unfold_to_non_existing_resources() { $this ->given( $parentHoaDirectory = dirname(dirname(dirname(dirname(__DIR__)))), $protocol = SUT::getInstance() ) ->when($result = $protocol->resolve('hoa://Library', false, true)) ->then ->array($result) ->isEqualTo([ $parentHoaDirectory . DS . 'hoathis', $parentHoaDirectory . DS . 'hoa' ]); } } given($wrapper = new SUT()) ->when($result = $wrapper->stream_cast(STREAM_CAST_FOR_SELECT)) ->then ->boolean($result) ->isFalse(); } public function case_stream_cast_as_stream() { $this ->given($wrapper = new SUT()) ->when($result = $wrapper->stream_cast(STREAM_CAST_AS_STREAM)) ->then ->boolean($result) ->isFalse(); } public function case_stream_close() { $this ->given( $wrapper = new SUT(), $this->openFile($wrapper) ) ->when($result = $wrapper->stream_close()) ->then ->variable($result) ->isNull() ->variable($wrapper->getStream()) ->isNull() ->variable($wrapper->getStreamName()) ->isNull(); } public function case_stream_not_eof() { $this ->given( $wrapper = new SUT(), $this->openFile($wrapper, 'foo'), fseek($wrapper->getStream(), 0, SEEK_SET) ) ->when($result = $wrapper->stream_eof()) ->then ->boolean($result) ->isFalse(); } public function case_stream_eof() { $this ->given( $this->function->feof = true, $wrapper = new SUT() ) ->when($result = $wrapper->stream_eof()) ->then ->boolean($result) ->isTrue(); } public function case_stream_flush() { $this ->given( $wrapper = new SUT(), $this->openFile($wrapper) ) ->when($result = $wrapper->stream_flush()) ->then ->boolean($result) ->isTrue(); } public function _case_stream_xxx_lock($operation) { $this ->given( $this->function->flock = function ($resource, $operation) use (&$_resource, &$_operation) { $_resource = $resource; $_operation = $operation; if ($operation === LOCK_NB) { return true; } return flock($resource, $operation); }, $wrapper = new SUT(), $this->openFile($wrapper) ) ->when($result = $wrapper->stream_lock($operation)) ->then ->boolean($result) ->isTrue() ->resource($_resource) ->isStream() ->isIdenticalTo($wrapper->getStream()) ->integer($_operation) ->isEqualTo($operation); } public function case_stream_shared_lock() { return $this->_case_stream_xxx_lock(LOCK_SH); } public function case_stream_exclusive_lock() { return $this->_case_stream_xxx_lock(LOCK_EX); } public function case_stream_release_lock() { return $this->_case_stream_xxx_lock(LOCK_UN); } public function case_stream_not_blocking_lock() { return $this->_case_stream_xxx_lock(LOCK_NB); } public function _case_metadata_touch_with_xxx_arguments($arguments, $path, $time, $atime) { $this ->given( $this->function->touch = function ($path, $time, $atime) use (&$_path, &$_time, &$_atime) { $_path = $path; $_time = $time; $_atime = $atime; return true; }, $wrapper = new SUT() ) ->when($result = $wrapper->stream_metadata($path, STREAM_META_TOUCH, $arguments)) ->then ->boolean($result) ->isTrue() ->string($_path) ->isEqualTo($path) ->variable($_time) ->isEqualTo($time) ->variable($_atime) ->isEqualTo($atime); } public function case_metadata_touch_with_no_argument() { return $this->_case_metadata_touch_with_xxx_arguments([], 'foo', null, null); } public function case_metadata_touch_with_time() { return $this->_case_metadata_touch_with_xxx_arguments([42], 'foo', 42, null); } public function case_metadata_touch_with_time_and_atime() { return $this->_case_metadata_touch_with_xxx_arguments([42, 777], 'foo', 42, 777); } public function _case_metadata_owner_xxx($owner) { $this ->given( $this->function->chown = function ($path, $user) use (&$_path, &$_user) { $_path = $path; $_user = $user; return true; }, $path = 'foo', $user = 'gordon', $wrapper = new SUT() ) ->when($result = $wrapper->stream_metadata('foo', $owner, $user)) ->then ->boolean($result) ->isTrue() ->string($path) ->isEqualTo($_path) ->string($user) ->isEqualTo($_user); } public function case_metadata_owner() { return $this->_case_metadata_owner_xxx(STREAM_META_OWNER); } public function case_metadata_owner_name() { return $this->_case_metadata_owner_xxx(STREAM_META_OWNER_NAME); } public function _case_metadata_group_xxx($grp) { $this ->given( $this->function->chgrp = function ($path, $group) use (&$_path, &$_group) { $_path = $path; $_group = $group; return true; }, $path = 'foo', $group = 'root', $wrapper = new SUT() ) ->when($result = $wrapper->stream_metadata('foo', $grp, $group)) ->then ->boolean($result) ->isTrue() ->string($path) ->isEqualTo($_path) ->string($group) ->isEqualTo($_group); } public function case_metadata_group() { return $this->_case_metadata_group_xxx(STREAM_META_GROUP); } public function case_metadata_group_name() { return $this->_case_metadata_group_xxx(STREAM_META_GROUP_NAME); } public function case_metadata_access() { $this ->given( $this->function->chmod = function ($path, $mode) use (&$_path, &$_mode) { $_path = $path; $_mode = $mode; return true; }, $path = 'foo', $mode = 0755, $wrapper = new SUT() ) ->when($result = $wrapper->stream_metadata('foo', STREAM_META_ACCESS, $mode)) ->then ->boolean($result) ->isTrue() ->string($path) ->isEqualTo($_path) ->integer($mode) ->isEqualTo($_mode); } public function case_metadata_default() { $this ->given( $option = 0, $mode = 0, $wrapper = new SUT() ) ->when($result = $wrapper->stream_metadata('foo', $option, $mode)) ->then ->boolean($result) ->isFalse(); } public function case_stream_open() { $this ->given( $this->function->fopen = function ($path, $mode, $options) use (&$_path, &$_mode, &$_options, &$_openedPath) { $_path = $path; $_mode = $mode; $_options = $options; return fopen($path, $mode, $options); }, $wrapper = new SUT(), $path = 'hoa://Test/Vfs/Foo?type=file', $mode = 'r', $options = STREAM_USE_PATH ) ->when($result = $wrapper->stream_open($path, $mode, $options, $openedPath)) ->then ->boolean($result) ->isTrue() ->string(SUT::realPath($path, true)) ->isEqualTo($_path) ->string($mode) ->isEqualTo($_mode) ->integer($options) ->isEqualTo($_options & STREAM_USE_PATH) ->resource($openedPath) ->isStream() ->isIdenticalTo($wrapper->getStream()) ->string($wrapper->getStreamName()) ->isEqualTo('atoum://Foo'); } public function case_stream_open_not_hoa_protocol() { $this ->given( $wrapper = new SUT(), $path = LUT::NO_RESOLUTION, $mode = 'r', $options = STREAM_USE_PATH ) ->when($result = $wrapper->stream_open($path, $mode, $options, $openedPath)) ->then ->boolean($result) ->isFalse(); } public function case_stream_open_not_a_resource() { $this ->given( $this->function->fopen = function ($path, $mode, $options) use (&$_path, &$_mode, &$_options, &$_openedPath) { $_path = $path; $_mode = $mode; $_options = $options; return fopen($path, $mode, $options); }, $this->function->is_resource = false, $wrapper = new SUT(), $path = 'hoa://Test/Vfs/Foo?type=file', $mode = 'r', $options = STREAM_USE_PATH ) ->when($result = $wrapper->stream_open($path, $mode, $options, $openedPath)) ->then ->boolean($result) ->isFalse() ->string(SUT::realPath($path, true)) ->isEqualTo($_path) ->string($mode) ->isEqualTo($_mode) ->integer($options) ->isEqualTo($_options & STREAM_USE_PATH) ->resource($openedPath) ->isStream(); } public function case_stream_read() { $this ->given( $this->function->fread = function ($resource, $count) use (&$_resource, &$_count) { $_resource = $resource; $_count = $count; return fread($resource, $count); }, $wrapper = new SUT(), $count = 42, $this->openFile($wrapper, str_repeat('@', $count)) ) ->when($result = $wrapper->stream_read($count)) ->then ->string($result) ->hasLength($count) ->resource($_resource) ->isStream() ->isIdenticalTo($wrapper->getStream()) ->integer($_count) ->isEqualTo($count); } public function _case_stream_seek_xxx($offset, $whence) { return $this ->given( $this->function->fseek = function ($resource, $offset, $whence) use (&$_resource, &$_offset, &$_whence) { $_resource = $resource; $_offset = $offset; $_whence = $whence; return fseek($resource, $offset, $whence); }, $wrapper = new SUT(), $this->openFile($wrapper, 'foobar') ) ->when($result = $wrapper->stream_seek($offset, $whence)) ->then ->boolean($result) ->isTrue() ->resource($_resource) ->isStream() ->isIdenticalTo($wrapper->getStream()) ->integer($offset) ->isEqualTo($_offset) ->integer($whence) ->isEqualTo($_whence) ->integer(ftell($wrapper->getStream())); } public function case_stream_seek_set() { return $this ->_case_stream_seek_xxx(3, SEEK_SET) ->isEqualTo(3); } public function case_stream_seek_current() { return $this ->_case_stream_seek_xxx(4, SEEK_CUR) ->isEqualTo(4); } public function case_stream_seek_end() { return $this ->_case_stream_seek_xxx(-4, SEEK_END) ->isEqualTo(2); } public function case_stream_stat() { $this ->given( $this->function->fstat = function ($resource) use (&$_resource) { $_resource = $resource; return fstat($resource); }, $wrapper = new SUT(), $this->openFile($wrapper) ) ->when($result = $wrapper->stream_stat()) ->then ->array($result) ->resource($_resource) ->isStream() ->isIdenticalTo($wrapper->getStream()); } public function case_stream_tell() { $this ->given( $this->function->ftell = function ($resource) use (&$_resource) { $_resource = $resource; return ftell($resource); }, $wrapper = new SUT(), $this->openFile($wrapper, 'foo'), $wrapper->stream_seek(2) ) ->when($result = $wrapper->stream_tell()) ->then ->integer($result) ->isEqualTo(2) ->resource($_resource) ->isStream() ->isIdenticalTo($wrapper->getStream()); } public function case_stream_truncate() { $this ->given( $this->function->ftruncate = function ($resource, $size) use (&$_resource, &$_size) { $_resource = $resource; $_size = $size; return ftruncate($resource, $size); }, $wrapper = new SUT(), $this->openFile($wrapper, 'foobar'), $size = 3 ) ->when($result = $wrapper->stream_truncate($size)) ->then ->boolean($result) ->isTrue() ->resource($_resource) ->isStream() ->isIdenticalTo($wrapper->getStream()) ->integer($size) ->isEqualTo($_size) ->integer($wrapper->stream_tell()) ->isEqualTo(0) ->let($wrapper->stream_seek(0, SEEK_END)) ->integer($wrapper->stream_tell()) ->isEqualTo(3); } public function case_stream_write() { $this ->given( $this->function->fwrite = function ($resource, $data) use (&$_resource, &$_data) { $_resource = $resource; $_data = $data; return fwrite($resource, $data); }, $wrapper = new SUT(), $wrapper->stream_open('hoa://Test/Vfs/Foo?type=file', 'wb+', STREAM_USE_PATH, $openedPath), $data = 'foo' ) ->when($result = $wrapper->stream_write($data)) ->then ->integer($result) ->isEqualTo(strlen($data)) ->resource($_resource) ->isStream() ->isIdenticalTo($wrapper->getStream()) ->string($_data) ->isEqualTo($data) ->let($wrapper->stream_seek(0)) ->string($wrapper->stream_read(3)) ->isEqualTo($data); } public function case_dir_closedir() { $this ->given( $wrapper = new SUT(), $this->openDirectory($wrapper) ) ->when($result = $wrapper->dir_closedir()) ->then ->variable($result) ->isNull() ->variable($wrapper->getStream()) ->isNull() ->variable($wrapper->getStreamName()) ->isNull(); } public function case_dir_opendir() { $this ->given( $this->function->opendir = function ($path) use (&$_path) { $_path = $path; return opendir($path); }, $wrapper = new SUT(), $path = 'hoa://Test/Vfs/Bar?type=directory', $options = 0 ) ->when($result = $wrapper->dir_opendir($path, $options)) ->then ->boolean($result) ->isTrue() ->string(SUT::realPath($path, true)) ->isEqualTo($_path) ->resource($wrapper->getStream()) ->isStream() ->string($wrapper->getStreamName()) ->isEqualTo('atoum://Bar'); } public function case_dir_opendir_not_a_resource() { $this ->given( $this->function->opendir = function ($path) use (&$_path) { $_path = $path; return false; }, $wrapper = new SUT(), $path = 'hoa://Test/Vfs/Bar?type=directory', $options = 0 ) ->when($result = $wrapper->dir_opendir($path, $options)) ->then ->boolean($result) ->isFalse() ->string(SUT::realPath($path, true)) ->isEqualTo($_path) ->variable($wrapper->getStream()) ->isNull() ->variable($wrapper->getStreamName()) ->isNull(); } public function case_dir_readdir() { $this ->given( $this->function->readdir = function ($resource) use (&$_resource) { $_resource = $resource; return readdir($resource); }, $wrapper = new SUT(), $this->openDirectory($wrapper, ['Baz', 'Qux']) ) ->when($result = $wrapper->dir_readdir()) ->then ->string($result) ->isEqualTo('Baz') ->resource($_resource) ->isIdenticalTo($wrapper->getStream()); } public function case_dir_readdir_until_eod() { $this ->given( $this->function->readdir = function ($resource) use (&$_resource) { $_resource = $resource; return readdir($resource); }, $wrapper = new SUT(), $this->openDirectory($wrapper, ['Baz', 'Qux']) ) ->when($result = $wrapper->dir_readdir()) ->then ->string($result) ->isEqualTo('Baz') ->resource($_resource) ->isIdenticalTo($wrapper->getStream()) ->when($result = $wrapper->dir_readdir()) ->then ->string($result) ->isEqualTo('Qux') ->when($result = $wrapper->dir_readdir()) ->then ->boolean($result) ->isFalse(); } public function case_dir_rewinddir() { $this ->given( $this->function->rewinddir = function ($resource) use (&$_resource) { $_resource = $resource; return rewinddir($resource); }, $wrapper = new SUT(), $this->openDirectory($wrapper, ['Baz']), $wrapper->dir_readdir() ) ->when($result = $wrapper->dir_rewinddir()) ->then ->variable($result) ->isNull() ->when($result = $wrapper->dir_readdir()) ->then ->string($result) ->isEqualTo('Baz') ->resource($_resource) ->isIdenticalTo($wrapper->getStream()); } public function case_dir_mkdir() { $this ->given( $this->function->mkdir = function ($path, $mode, $options) use (&$_path, &$_mode, &$_options) { $_path = $path; $_mode = $mode; $_options = $options; return true; }, $wrapper = new SUT(), $this->openDirectory($wrapper), $path = 'Baz', $mode = 0755, $options = STREAM_MKDIR_RECURSIVE ) ->when($result = $wrapper->mkdir($path, $mode, $options)) ->then ->boolean($result) ->isTrue() ->string($_path) ->isEqualTo($path) ->integer($_mode) ->isEqualTo($_mode) ->integer($_options) ->isEqualTo($options | STREAM_MKDIR_RECURSIVE); } public function case_rename() { $this ->given( $this->function->rename = function ($from, $to) use (&$_from, &$_to) { $_to = $to; $_from = $from; return rename($from, $to); }, $wrapper = new SUT(), $this->openFile($wrapper), $from = 'hoa://Test/Vfs/Foo?type=file', $to = 'hoa://Test/Vfs/Oof?type=file' ) ->when($result = $wrapper->rename($from, $to)) ->then ->boolean($result) ->isTrue() ->string($_from) ->isEqualTo(SUT::realPath($from)) ->string($_to) ->isEqualTo(SUT::realPath($_to, false)); } public function case_rmdir() { $this ->given( $this->function->rmdir = function ($path) use (&$_path) { $_path = $path; return rmdir($path); }, $wrapper = new SUT(), $this->openDirectory($wrapper) ) ->when($result = $wrapper->rmdir('hoa://Test/Vfs/Bar?type=directory', 0)) ->then ->boolean($result) ->isTrue(); } public function case_rmdir_a_file() { $this ->given( $wrapper = new SUT(), $this->openFile($wrapper) ) ->when($result = $wrapper->rmdir('hoa://Test/Vfs/Foo?type=file', 0)) ->then ->boolean($result) ->isFalse(); } public function case_unlink() { $this ->given( $wrapper = new SUT(), $this->openFile($wrapper) ) ->when($result = $wrapper->unlink('hoa://Test/Vfs/Foo?type=file')) ->then ->boolean($result) ->isTrue(); } public function case_rmdir_a_directory() { $this ->given( $wrapper = new SUT(), $this->openDirectory($wrapper) ) ->when($result = $wrapper->unlink('hoa://Test/Vfs/Bar?type=directory')) ->then ->boolean($result) ->isFalse(); } public function case_url_stat() { $this ->given( $this->function->stat = function ($path) use (&$_path) { $_path = $path; return stat($path); }, $wrapper = new SUT(), $this->openFile($wrapper), $path = 'hoa://Test/Vfs/Foo?type=file' ) ->when($result = $wrapper->url_stat($path, 0)) ->then ->let( $keys = [ 'dev', 'ino', 'mode', 'nlink', 'uid', 'gid', 'rdev', 'size', 'atime', 'mtime', 'ctime', 'blksize', 'blocks' ] ) ->array($result) ->hasSize(26) ->hasKeys($keys) ->hasKeys(array_keys($keys)) ->string($_path) ->isEqualTo(SUT::realPath($path)); } public function case_url_stat_not_hoa_protocol() { $this ->given( $wrapper = new SUT(), $path = LUT::NO_RESOLUTION ) ->when(function () use ($wrapper, $path) { $wrapper->url_stat($path, 0); }) ->then ->error() ->exists(); } protected function openFile(SUT $wrapper, $content = '') { $wrapper->stream_open('hoa://Test/Vfs/Foo?type=file', 'wb+', STREAM_USE_PATH, $openedPath); fwrite($openedPath, $content, strlen($content)); fseek($openedPath, 0, SEEK_SET); return $wrapper; } protected function openDirectory(SUT $wrapper, array $children = []) { $wrapper->dir_opendir('hoa://Test/Vfs/Bar?type=directory', 0); foreach ($children as $child) { resolve('hoa://Test/Vfs/Bar/' . $child . '?type=file'); } return $wrapper; } } resolve($path, $exists); } public function stream_cast($castAs) { return false; } public function stream_close() { if (true === @fclose($this->getStream())) { $this->_stream = null; $this->_streamName = null; } return; } public function stream_eof() { return feof($this->getStream()); } public function stream_flush() { return fflush($this->getStream()); } public function stream_lock($operation) { return flock($this->getStream(), $operation); } public function stream_metadata($path, $option, $values) { $path = static::realPath($path, false); switch ($option) { case STREAM_META_TOUCH: $arity = count($values); if (0 === $arity) { $out = touch($path); } elseif (1 === $arity) { $out = touch($path, $values[0]); } else { $out = touch($path, $values[0], $values[1]); } break; case STREAM_META_OWNER_NAME: case STREAM_META_OWNER: $out = chown($path, $values); break; case STREAM_META_GROUP_NAME: case STREAM_META_GROUP: $out = chgrp($path, $values); break; case STREAM_META_ACCESS: $out = chmod($path, $values); break; default: $out = false; } return $out; } public function stream_open($path, $mode, $options, &$openedPath) { $path = static::realPath($path, 'r' === $mode[0]); if (Protocol::NO_RESOLUTION === $path) { return false; } if (null === $this->context) { $openedPath = fopen($path, $mode, $options & STREAM_USE_PATH); } else { $openedPath = fopen( $path, $mode, $options & STREAM_USE_PATH, $this->context ); } if (false === is_resource($openedPath)) { return false; } $this->_stream = $openedPath; $this->_streamName = $path; return true; } public function stream_read($count) { return fread($this->getStream(), $count); } public function stream_seek($offset, $whence = SEEK_SET) { return 0 === fseek($this->getStream(), $offset, $whence); } public function stream_stat() { return fstat($this->getStream()); } public function stream_tell() { return ftell($this->getStream()); } public function stream_truncate($size) { return ftruncate($this->getStream(), $size); } public function stream_write($data) { return fwrite($this->getStream(), $data); } public function dir_closedir() { closedir($this->getStream()); $this->_stream = null; $this->_streamName = null; return; } public function dir_opendir($path, $options) { $path = static::realPath($path); $handle = null; if (null === $this->context) { $handle = @opendir($path); } else { $handle = @opendir($path, $this->context); } if (false === $handle) { return false; } $this->_stream = $handle; $this->_streamName = $path; return true; } public function dir_readdir() { return readdir($this->getStream()); } public function dir_rewinddir() { return rewinddir($this->getStream()); } public function mkdir($path, $mode, $options) { if (null === $this->context) { return mkdir( static::realPath($path, false), $mode, $options | STREAM_MKDIR_RECURSIVE ); } return mkdir( static::realPath($path, false), $mode, $options | STREAM_MKDIR_RECURSIVE, $this->context ); } public function rename($from, $to) { if (null === $this->context) { return rename(static::realPath($from), static::realPath($to, false)); } return rename( static::realPath($from), static::realPath($to, false), $this->context ); } public function rmdir($path, $options) { if (null === $this->context) { return rmdir(static::realPath($path)); } return rmdir(static::realPath($path), $this->context); } public function unlink($path) { if (null === $this->context) { return unlink(static::realPath($path)); } return unlink(static::realPath($path), $this->context); } public function url_stat($path, $flags) { $path = static::realPath($path); if (Protocol::NO_RESOLUTION === $path) { if ($flags & STREAM_URL_STAT_QUIET) { return 0; } else { return trigger_error( 'Path ' . $path . ' cannot be resolved.', E_WARNING ); } } if ($flags & STREAM_URL_STAT_LINK) { return @lstat($path); } return @stat($path); } public function getStream() { return $this->_stream; } public function getStreamName() { return $this->_streamName; } } stream_wrapper_register('hoa', Wrapper::class); } namespace { if (!function_exists('resolve')) { function resolve($path, $exists = true, $unfold = false) { return Hoa\Protocol::getInstance()->resolve($path, $exists, $unfold); } } } setType($is); if (self::IS_A_BRIGADE === $this->getType()) { $this->setBrigade($brigade); } else { $this->setBucket(stream_bucket_new($brigade, $buffer)); $bucket = $this->getBucket(); $this->setBrigade($bucket); } return; } public function eob() { $this->_bucket = null; return false == $this->getBucket(); } public function append(Bucket $bucket) { stream_bucket_append($this->getBrigade(), $bucket->getBucket()); return; } public function prepend(Bucket $bucket) { stream_bucket_prepend($this->getBrigade(), $bucket->getBucket()); return; } protected function setType($type) { $old = $this->_type; $this->_type = $type; return $old; } public function getType() { return $this->_type; } public function setData($data) { $old = $this->getBucket()->data; $this->getBucket()->data = $data; $this->getBucket()->datalen = strlen($this->getBucket()->data); return $old; } public function getData() { if (null === $this->getBucket()) { return null; } return $this->getBucket()->data; } public function getLength() { if (null === $this->getBucket()) { return 0; } return $this->getBucket()->datalen; } protected function setBrigade(&$brigade) { $old = $this->_brigade; $this->_brigade = $brigade; return $old; } public function getBrigade() { return $this->_brigade; } protected function setBucket($bucket) { $old = $this->_bucket; $this->_bucket = $bucket; return $old; } protected function getBucket() { if (null === $this->_bucket && self::IS_A_BRIGADE === $this->getType()) { $this->_bucket = stream_bucket_make_writeable($this->getBrigade()); } return $this->_bucket; } } _stream; $this->_stream = $stream; return $old; } public function getStream() { return $this->_stream; } protected function setInnerStream(Stream $innerStream) { $old = $this->_innerStream; $this->_innerStream = $innerStream; return $old; } public function getInnerStream() { return $this->_innerStream; } } _id = $id; $this->_context = stream_context_create(); return; } public static function getInstance($id) { if (empty($id)) { throw new Exception('Context ID must not be null.', 0); } if (false === static::contextExists($id)) { static::$_instances[$id] = new static($id); } return static::$_instances[$id]; } public function getId() { return $this->_id; } public static function contextExists($id) { return array_key_exists($id, static::$_instances); } public function setOptions(array $options) { return stream_context_set_option($this->getContext(), $options); } public function setParameters(array $parameters) { return stream_context_set_params($this->getContext(), $parameters); } public function getOptions() { return stream_context_get_options($this->getContext()); } public function getParameters() { return stream_context_get_params($this->getContext()); } public function getContext() { return $this->_context; } } eob()) { $consumed += $iBucket->getLength(); $oBucket->append($iBucket); } unset($iBucket); unset($oBucket); return self::PASS_ON; } public function onCreate() { return true; } public function onClose() { return; } public function setName($name) { $old = $this->filtername; $this->filtername = $name; return $old; } public function setParameters($parameters) { $old = $this->params; $this->params = $parameters; return $old; } public function getName() { return $this->filtername; } public function getParameters() { return $this->params; } public function getStream() { return isset($this->stream) ? $this->stream : null; } } getStream(); } if (null === $parameters) { return self::$_resources[$name] = stream_filter_append( $stream, $name, $mode ); } return self::$_resources[$name] = stream_filter_append( $stream, $name, $mode, $parameters ); } public static function prepend( $stream, $name, $mode = self::READ, $parameters = null ) { if ($stream instanceof Stream) { $stream = $stream->getStream(); } if (null === $parameters) { return self::$_resources[$name] = stream_filter_prepend( $stream, $name, $mode ); } return self::$_resources[$name] = stream_filter_prepend( $stream, $name, $mode, $parameters ); } public static function remove($streamFilter) { if (!is_resource($streamFilter)) { if (isset(self::$_resources[$streamFilter])) { $streamFilter = self::$_resources[$streamFilter]; } else { throw new Exception( 'Cannot remove the stream filter %s because no resource was ' . 'found with this name.', 3, $streamFilter ); } } return stream_filter_remove($streamFilter); } public static function isRegistered($name) { return in_array($name, self::getRegistered()); } public static function getRegistered() { return stream_get_filters(); } } Consistency::flexEntity('Hoa\Stream\Filter\Filter'); eob()) { $this->_buffer .= $iBucket->getData(); $consumed += $iBucket->getLength(); } if (null !== $consumed) { $return = self::PASS_ON; } if (true === $closing) { $stream = $this->getStream(); $this->compute(); $bucket = new Stream\Bucket( $stream, Stream\Bucket::IS_A_STREAM, $this->_buffer ); $oBucket = new Stream\Bucket($out); $oBucket->append($bucket); $return = self::PASS_ON; $this->_buffer = null; } return $return; } abstract protected function compute(); } _streamName = $streamName; $this->_context = $context; $this->_hasBeenDeferred = $wait; $this->setListener( new Event\Listener( $this, [ 'authrequire', 'authresult', 'complete', 'connect', 'failure', 'mimetype', 'progress', 'redirect', 'resolve', 'size' ] ) ); if (true === $wait) { return; } $this->open(); return; } final private static function &_getStream( $streamName, Stream $handler, $context = null ) { $name = md5($streamName); if (null !== $context) { if (false === Context::contextExists($context)) { throw new Exception( 'Context %s was not previously declared, cannot retrieve ' . 'this context.', 0, $context ); } $context = Context::getInstance($context); } if (!isset(self::$_register[$name])) { self::$_register[$name] = [ self::NAME => $streamName, self::HANDLER => $handler, self::RESOURCE => $handler->_open($streamName, $context), self::CONTEXT => $context ]; Event::register( 'hoa://Event/Stream/' . $streamName, $handler ); Event::register( 'hoa://Event/Stream/' . $streamName . ':close-before', $handler ); } else { $handler->_borrowing = true; } if (null === self::$_register[$name][self::RESOURCE]) { self::$_register[$name][self::RESOURCE] = $handler->_open($streamName, $context); } return self::$_register[$name]; } abstract protected function &_open($streamName, Context $context = null); abstract protected function _close(); final public function open() { $context = $this->_context; if (true === $this->hasBeenDeferred()) { if (null === $context) { $handle = Context::getInstance(uniqid()); $handle->setParameters([ 'notification' => [$this, '_notify'] ]); $context = $handle->getId(); } elseif (true === Context::contextExists($context)) { $handle = Context::getInstance($context); $parameters = $handle->getParameters(); if (!isset($parameters['notification'])) { $handle->setParameters([ 'notification' => [$this, '_notify'] ]); } } } $this->_bufferSize = self::DEFAULT_BUFFER_SIZE; $this->_bucket = self::_getStream( $this->_streamName, $this, $context ); return $this; } final public function close() { $streamName = $this->getStreamName(); $name = md5($streamName); if (!isset(self::$_register[$name])) { return; } Event::notify( 'hoa://Event/Stream/' . $streamName . ':close-before', $this, new Event\Bucket() ); if (false === $this->_close()) { return; } unset(self::$_register[$name]); $this->_bucket[self::HANDLER] = null; Event::unregister( 'hoa://Event/Stream/' . $streamName ); Event::unregister( 'hoa://Event/Stream/' . $streamName . ':close-before' ); return; } public function getStreamName() { if (empty($this->_bucket)) { return null; } return $this->_bucket[self::NAME]; } public function getStream() { if (empty($this->_bucket)) { return null; } return $this->_bucket[self::RESOURCE]; } public function getStreamContext() { if (empty($this->_bucket)) { return null; } return $this->_bucket[self::CONTEXT]; } public static function getStreamHandler($streamName) { $name = md5($streamName); if (!isset(self::$_register[$name])) { return null; } return self::$_register[$name][self::HANDLER]; } public function _setStream($stream) { if (false === is_resource($stream) && ('resource' !== gettype($stream) || 'Unknown' !== get_resource_type($stream))) { throw new Exception( 'Try to change the stream resource with an invalid one; ' . 'given %s.', 1, gettype($stream) ); } $old = $this->_bucket[self::RESOURCE]; $this->_bucket[self::RESOURCE] = $stream; return $old; } public function isOpened() { return is_resource($this->getStream()); } public function setStreamTimeout($seconds, $microseconds = 0) { return stream_set_timeout($this->getStream(), $seconds, $microseconds); } protected function hasBeenDeferred() { return $this->_hasBeenDeferred; } public function hasTimedOut() { $metaData = $this->getStreamMetaData(); return true === $metaData['timed_out']; } public function setStreamBlocking($mode) { return stream_set_blocking($this->getStream(), (int) $mode); } public function setStreamBuffer($buffer) { $out = 0 === stream_set_write_buffer($this->getStream(), $buffer); if (true === $out) { $this->_bufferSize = $buffer; } return $out; } public function disableStreamBuffer() { return $this->setStreamBuffer(0); } public function getStreamBufferSize() { return $this->_bufferSize; } public function getStreamWrapperName() { if (false === $pos = strpos($this->getStreamName(), '://')) { return 'file'; } return substr($this->getStreamName(), 0, $pos); } public function getStreamMetaData() { return stream_get_meta_data($this->getStream()); } public function isBorrowing() { return $this->_borrowing; } public function _notify( $ncode, $severity, $message, $code, $transferred, $max ) { static $_map = [ STREAM_NOTIFY_AUTH_REQUIRED => 'authrequire', STREAM_NOTIFY_AUTH_RESULT => 'authresult', STREAM_NOTIFY_COMPLETED => 'complete', STREAM_NOTIFY_CONNECT => 'connect', STREAM_NOTIFY_FAILURE => 'failure', STREAM_NOTIFY_MIME_TYPE_IS => 'mimetype', STREAM_NOTIFY_PROGRESS => 'progress', STREAM_NOTIFY_REDIRECTED => 'redirect', STREAM_NOTIFY_RESOLVE => 'resolve', STREAM_NOTIFY_FILE_SIZE_IS => 'size' ]; $this->getListener()->fire($_map[$ncode], new Event\Bucket([ 'code' => $code, 'severity' => $severity, 'message' => $message, 'transferred' => $transferred, 'max' => $max ])); return; } final public static function _Hoa_Stream() { foreach (self::$_register as $entry) { $entry[self::HANDLER]->close(); } return; } public function __toString() { return $this->getStreamName(); } public function __destruct() { if (false === $this->isOpened()) { return; } $this->close(); return; } } class _Protocol extends Protocol\Node { protected $_name = 'Stream'; public function reachId($id) { return Stream::getStreamHandler($id); } } Consistency::flexEntity('Hoa\Stream\Stream'); Consistency::registerShutdownFunction(xcallable('Hoa\Stream\Stream::_Hoa_Stream')); $protocol = Protocol::getInstance(); $protocol['Library'][] = new _Protocol(); given( $filename = 'hoa://Test/Vfs/Foo?type=file', $content = 'Hello, World!', file_put_contents($filename, $content), $stream = fopen($filename, 'r'), $name = 'string.toupper' ) ->when( SUT::append($stream, $name), $result = stream_get_contents($stream) ) ->then ->string($result) ->isEqualTo(strtoupper($content)); } public function case_prepend() { $this ->given( $filename = 'hoa://Test/Vfs/Foo?type=file', $content = 'Hello, World!', file_put_contents($filename, $content), $stream = fopen($filename, 'r'), $name = 'string.toupper' ) ->when( SUT::prepend($stream, $name), $result = stream_get_contents($stream) ) ->then ->string($result) ->isEqualTo(strtoupper($content)); } public function case_append_append() { $this ->given( $filename = 'hoa://Test/Vfs/Foo?type=file', $content = 'Hello, World!', file_put_contents($filename, $content), $stream = fopen($filename, 'r'), $name1 = 'string.toupper', $name2 = 'string.tolower' ) ->when( SUT::append($stream, $name1), SUT::append($stream, $name2), $result = stream_get_contents($stream) ) ->then ->string($result) ->isEqualTo(strtolower($content)); } public function case_append_prepend() { $this ->given( $filename = 'hoa://Test/Vfs/Foo?type=file', $content = 'Hello, World!', file_put_contents($filename, $content), $stream = fopen($filename, 'r'), $name1 = 'string.toupper', $name2 = 'string.tolower' ) ->when( SUT::append($stream, $name1), SUT::prepend($stream, $name2), $result = stream_get_contents($stream) ) ->then ->string($result) ->isEqualTo(strtoupper($content)); } public function case_prepend_prepend() { $this ->given( $filename = 'hoa://Test/Vfs/Foo?type=file', $content = 'Hello, World!', file_put_contents($filename, $content), $stream = fopen($filename, 'r'), $name1 = 'string.toupper', $name2 = 'string.tolower' ) ->when( SUT::prepend($stream, $name1), SUT::prepend($stream, $name2), $result = stream_get_contents($stream) ) ->then ->string($result) ->isEqualTo(strtoupper($content)); } public function case_append_1000_filters() { $this ->given( $filename = 'hoa://Test/Vfs/Foo?type=file', $content = 'Hello, World!', file_put_contents($filename, $content), $stream = fopen($filename, 'r'), $name = 'string.toupper' ) ->when(function () use ($stream, $name) { for ($i = 1000; $i >= 0; --$i) { $this->resource(SUT::prepend($stream, $name)); } }) ->when($result = stream_get_contents($stream)) ->then ->string($result) ->isEqualTo(strtoupper($content)); } } given( $name = 'custom', SUT::register($name, CustomFilter::class), $filename = 'hoa://Test/Vfs/Foo?type=file', $content = 'Hello, World!', file_put_contents($filename, $content), $stream = fopen($filename, 'r') ) ->when( SUT::append($stream, $name), $result = stream_get_contents($stream) ) ->then ->string($result) ->isEqualTo( strtolower($content) . ' ' . strlen($content) ); } } class CustomFilter extends LUT\Filter\LateComputed { protected function compute() { $this->_buffer = strtolower($this->_buffer) . ' ' . strlen($this->_buffer); return; } } given( $port = mt_rand(10000, 12000), exec( sprintf( 'php -S 127.0.0.1:%d -t %s > /dev/null 2>&1 & echo $! && sleep 0.2', $port, dirname(__DIR__) . DS . 'Fixtures' ), $outputs ), $pid = $outputs[0], $stream = new SUT('http://127.0.0.1:' . $port, null, true), $stream->on( 'connect', function (Event\Bucket $bucket) use ($self, &$connectCalled) { $connectCalled = true; $data = $bucket->getData(); $self ->array($data) ->isEqualTo([ 'code' => 0, 'severity' => 0, 'message' => null, 'transferred' => 0, 'max' => 0 ]); } ), $stream->on( 'mimetype', function (Event\Bucket $bucket) use ($self, &$mimetypeCalled) { $mimetypeCalled = true; $data = $bucket->getData(); $self ->array($data) ->isEqualTo([ 'code' => 0, 'severity' => 0, 'message' => 'text/html; charset=UTF-8', 'transferred' => 0, 'max' => 0 ]); } ), $stream->on( 'size', function (Event\Bucket $bucket) use ($self, &$sizeCalled) { $sizeCalled = true; $data = $bucket->getData(); $self ->array($data) ->isEqualTo([ 'code' => 0, 'severity' => 0, 'message' => 'Content-Length: 14', 'transferred' => 0, 'max' => 14 ]); } ), $stream->on( 'progress', function (Event\Bucket $bucket) use ($self, &$progressCalled) { $progressCalled = true; $data = $bucket->getData(); $self ->array($data) ->isEqualTo([ 'code' => 0, 'severity' => 0, 'message' => null, 'transferred' => 0, 'max' => 14 ]); } ) ) ->when($stream->open()) ->then ->boolean($connectCalled) ->isTrue() ->boolean($mimetypeCalled) ->isTrue() ->boolean($sizeCalled) ->isTrue() ->boolean($progressCalled) ->isTrue() ->let(!empty($pid) && exec('kill ' . $pid)); } } class SUT extends LUT\Stream { protected function &_open($streamName, LUT\Context $context = null) { if (null === $context) { $out = fopen($streamName, 'rb'); } else { $out = fopen($streamName, 'rb', false, $context->getContext()); } return $out; } protected function _close() { return fclose($this->getStream()); } } boolean(SUT::IS_A_BRIGADE) ->isTrue() ->boolean(SUT::IS_A_STREAM) ->isFalse(); } public function case_construct_a_brigade() { $this ->given($brigade = 'foo') ->when($result = new SUT($brigade, SUT::IS_A_BRIGADE)) ->then ->boolean($result->getType()) ->isEqualTo(SUT::IS_A_BRIGADE) ->variable($result->getBrigade()) ->isIdenticalTo($brigade) ->isIdenticalTo('foo'); } public function case_construct_a_stream() { $this ->given( $stream = fopen(__FILE__, 'r'), $buffer = 'bar' ) ->when($result = new SUT($stream, SUT::IS_A_STREAM, $buffer)) ->then ->boolean($result->getType()) ->isEqualTo(SUT::IS_A_STREAM) ->let($bucket = $this->invoke($result)->getBucket()) ->object($bucket) ->isInstanceOf(\StdClass::class) ->resource($bucket->bucket) ->string($bucket->data) ->isEqualTo($buffer) ->integer($bucket->datalen) ->isEqualTo(strlen($buffer)) ->object($result->getBrigade()) ->isIdenticalTo($bucket); } public function case_eob() { $this ->given( $stream = fopen(__FILE__, 'r'), $bucket = new SUT($stream, SUT::IS_A_STREAM) ) ->when($result = $bucket->eob()) ->then ->boolean($result) ->isTrue(); } public function case_set_data() { $this ->given( $stream = fopen(__FILE__, 'r'), $oldBuffer = 'bar', $bucket = new SUT($stream, SUT::IS_A_STREAM, $oldBuffer), $buffer = 'bazqux' ) ->when($result = $bucket->setData('bazqux')) ->then ->string($result) ->isEqualTo($oldBuffer) ->let($_bucket = $this->invoke($bucket)->getBucket()) ->object($_bucket) ->isInstanceOf(\StdClass::class) ->resource($_bucket->bucket) ->string($_bucket->data) ->isEqualTo($buffer) ->integer($_bucket->datalen) ->isEqualTo(strlen($buffer)) ->object($bucket->getBrigade()) ->isIdenticalTo($_bucket); } public function case_get_data() { $this ->given( $stream = fopen(__FILE__, 'r'), $buffer = 'bar', $bucket = new SUT($stream, SUT::IS_A_STREAM, $buffer) ) ->when($result = $bucket->getData()) ->then ->string($result) ->isEqualTo($buffer) ->isEqualTo($this->invoke($bucket)->getBucket()->data); } public function case_get_length() { $this ->given( $stream = fopen(__FILE__, 'r'), $buffer = 'bar', $bucket = new SUT($stream, SUT::IS_A_STREAM, $buffer) ) ->when($result = $bucket->getLength()) ->then ->integer($result) ->isEqualTo(strlen($buffer)) ->isEqualTo($this->invoke($bucket)->getBucket()->datalen); } } given( $stream = new \StdClass(), $composite = new SUT() ) ->when($result = $this->invoke($composite)->setStream($stream)) ->then ->variable($result) ->isNull(); } public function case_get_stream() { $this ->given( $stream = new \StdClass(), $composite = new SUT(), $this->invoke($composite)->setStream($stream) ) ->when($result = $composite->getStream()) ->then ->object($result) ->isIdenticalTo($stream); } public function case_set_inner_stream() { $this ->given( $innerStream = new \Mock\Hoa\Stream(__FILE__), $composite = new SUT() ) ->when($result = $this->invoke($composite)->setInnerStream($innerStream)) ->then ->variable($result) ->isNull(); } public function case_get_inner_stream() { $this ->given( $innerStream = new \Mock\Hoa\Stream(__FILE__), $composite = new SUT(), $this->invoke($composite)->setInnerStream($innerStream) ) ->when($result = $composite->getInnerStream()) ->then ->object($result) ->isIdenticalTo($innerStream); } } exception(function () { SUT::getInstance(null); }) ->isInstanceOf(LUT\Exception::class); } public function case_get_new_instance() { $this ->when($result = SUT::getInstance('foo')) ->then ->object($result) ->isInstanceOf(SUT::class); } public function case_get_new_instances() { $this ->when($result = SUT::getInstance('foo')) ->then ->object($result) ->isNotIdenticalTo(SUT::getInstance('bar')); } public function case_get_same_instance() { $this ->when($result = SUT::getInstance('foo')) ->then ->object($result) ->isIdenticalTo(SUT::getInstance('foo')); } public function case_get_id() { $this ->given( $id = 'foo', $context = SUT::getInstance($id) ) ->when($result = $context->getId()) ->then ->string($result) ->isEqualTo($id); } public function case_context_exists() { $this ->given( $id = 'foo', SUT::getInstance($id) ) ->when($result = SUT::contextExists($id)) ->then ->boolean($result) ->isTrue(); } public function case_context_does_not_exist() { $this ->when($result = SUT::contextExists('foo')) ->then ->boolean($result) ->isFalse(); } public function case_set_options() { $this ->given( $context = SUT::getInstance('foo'), $options = ['bar' => ['baz' => 'qux']] ) ->when($result = $context->setOptions($options)) ->then ->boolean($result) ->isTrue(); } public function case_get_options() { $this ->given( $context = SUT::getInstance('foo'), $options = ['bar' => ['baz' => 'qux']], $context->setOptions($options) ) ->when($result = $context->getOptions()) ->then ->array($result) ->isEqualTo($options); } public function case_set_parameters() { $this ->given( $context = SUT::getInstance('foo'), $parameters = [ 'notificaion' => 'callback', 'options' => ['bar' => ['baz' => 'qux']] ] ) ->when($result = $context->setParameters($parameters)) ->then ->boolean($result) ->isTrue(); } public function case_get_parameters() { $this ->given( $context = SUT::getInstance('foo'), $parameters = [ 'notification' => 'callback', 'options' => ['bar' => ['baz' => 'qux']] ], $context->setParameters($parameters) ) ->when($result = $context->getParameters()) ->then ->array($result) ->isEqualTo($parameters); } public function case_get_context() { $this ->given($context = SUT::getInstance('foo')) ->when($result = $context->getContext()) ->then ->resource($result) ->isStreamContext(); } } when($result = new SUT('foo', 0)) ->then ->object($result) ->isInstanceOf(HoaException::class); } } integer(SUT::PASS_ON) ->isEqualTo(PSFS_PASS_ON) ->integer(SUT::FEED_ME) ->isEqualTo(PSFS_FEED_ME) ->integer(SUT::FATAL_ERROR) ->isEqualTo(PSFS_ERR_FATAL) ->integer(SUT::FLAG_NORMAL) ->isEqualTo(PSFS_FLAG_NORMAL) ->integer(SUT::FLAG_FLUSH_INC) ->isEqualTo(PSFS_FLAG_FLUSH_INC) ->integer(SUT::FLAG_FLUSH_CLOSE) ->isEqualTo(PSFS_FLAG_FLUSH_CLOSE); } public function case_is_a_php_filter() { $this ->when($result = new SUT()) ->then ->object($result) ->isInstanceOf(\php_user_filter::class); } public function case_interfaces() { $this ->when($result = new SUT()) ->then ->object($result) ->isInstanceOf(LUT\IStream\Stream::class); } public function case_set_name() { $this ->given($filter = new SUT()) ->when($result = $filter->setName('foo')) ->then ->string($result) ->isEqualTo(''); } public function case_get_name() { $this ->given( $filter = new SUT(), $name = 'foo', $filter->setName($name) ) ->when($result = $filter->getName()) ->then ->string($result) ->isEqualTo($name); } public function case_set_parameters() { $this ->given($filter = new SUT()) ->when($result = $filter->setParameters(['foo', 'bar', 'baz'])) ->then ->string($result) ->isEqualTo(''); } public function case_get_parameters() { $this ->given( $filter = new SUT(), $parameters = ['foo', 'bar', 'baz'], $filter->setParameters($parameters) ) ->when($result = $filter->getParameters()) ->then ->array($result) ->isEqualTo($parameters); } public function case_get_stream() { $this ->given($filter = new SUT()) ->when($result = $filter->getStream()) ->then ->variable($result) ->isNull(); } } when($result = new SUT('foo', 0)) ->then ->object($result) ->isInstanceOf(LUT\Exception::class); } } boolean(SUT::OVERWRITE) ->isTrue() ->boolean(SUT::DO_NOT_OVERWRITE) ->isFalse() ->integer(SUT::READ) ->isEqualTo(STREAM_FILTER_READ) ->integer(SUT::WRITE) ->isEqualTo(STREAM_FILTER_WRITE) ->integer(SUT::READ_AND_WRITE) ->isEqualTo(STREAM_FILTER_ALL); } public function case_register() { $this ->when($result = SUT::register('foo', \StdClass::class)) ->then ->boolean($result) ->isTrue(); } public function case_register_already_registered_do_not_overwrite() { $this ->given( $name = 'foo', $class = \StdClass::class, SUT::register($name, $class) ) ->exception(function () use ($name, $class) { SUT::register($name, $class); }) ->isInstanceOf(LUT\Filter\Exception::class) ->hasMessage('Filter foo is already registered.'); } public function case_register_already_registered_do_overwrite() { $this ->given( $name = 'foo', SUT::register($name, \StdClass::class), new \Mock\StdClass() ) ->when($result = SUT::register($name, \Mock\StdClass::class, SUT::OVERWRITE)) ->then ->boolean($result) ->isFalse(); } public function case_register_empty_name() { $this ->exception(function () { SUT::register('', \StdClass::class); }) ->isInstanceOf(LUT\Filter\Exception::class) ->hasMessage( 'Filter name cannot be empty ' . '(implementation class is StdClass).' ); } public function case_register_unknown_class() { $this ->exception(function () { SUT::register('foo', '42Foo'); }) ->isInstanceOf(LUT\Filter\Exception::class) ->hasMessage( 'Cannot register the 42Foo class for the filter foo ' . 'because it does not exist.' ); } public function case_append() { $this ->given( $stream = fopen('hoa://Test/Vfs/Foo?type=file', 'r'), $name = 'string.toupper' ) ->when($result = SUT::append($stream, $name)) ->then ->resource($result) ->isStreamFilter(); } public function case_prepend() { $this ->given( $stream = fopen('hoa://Test/Vfs/Foo?type=file', 'r'), $name = 'string.toupper' ) ->when($result = SUT::prepend($stream, $name)) ->then ->resource($result) ->isStreamFilter(); } public function case_remove() { $this ->given( $stream = fopen('hoa://Test/Vfs/Foo?type=file', 'r'), $name = 'string.toupper', $filter = SUT::append($stream, $name) ) ->when($result = SUT::remove($filter)) ->then ->boolean($result) ->isTrue(); } public function case_remove_by_name() { $this ->given( $stream = fopen('hoa://Test/Vfs/Foo?type=file', 'r'), $name = 'string.toupper', $filter = SUT::append($stream, $name) ) ->when($result = SUT::remove($name)) ->then ->boolean($result) ->isTrue(); } public function case_remove_unknown() { $this ->exception(function () { SUT::remove('foo'); }) ->isInstanceOf(LUT\Filter\Exception::class) ->hasMessage( 'Cannot remove the stream filter foo ' . 'because no resource was found with this name.' ); } public function case_is_registered() { $this ->when($result = SUT::isRegistered('string.toupper')) ->then ->boolean($result) ->isTrue(); } public function case_is_not_registered() { $this ->when($result = SUT::isRegistered('foo')) ->then ->boolean($result) ->isFalse(); } public function case_get_registered() { $this ->when($result = SUT::getRegistered()) ->then ->array($result) ->containsValues([ 'string.rot13', 'string.toupper', 'string.tolower', 'string.strip_tags', 'consumed', 'dechunk' ]); } } when($result = new \Mock\Hoa\Stream\IStream\Bufferable()) ->then ->object($result) ->isInstanceOf(SUT::class) ->isInstanceOf(LUT\IStream\Stream::class); } } when($result = new \Mock\Hoa\Stream\IStream\In()) ->then ->object($result) ->isInstanceOf(SUT::class) ->isInstanceOf(LUT\IStream\Stream::class); } } when($result = new \Mock\Hoa\Stream\IStream\Lockable()) ->then ->object($result) ->isInstanceOf(SUT::class) ->isInstanceOf(LUT\IStream\Stream::class); } public function case_constants() { $this ->when($result = SUT::LOCK_SHARED) ->then ->integer($result) ->isEqualTo(LOCK_SH) ->when($result = SUT::LOCK_EXCLUSIVE) ->then ->integer($result) ->isEqualTo(LOCK_EX) ->when($result = SUT::LOCK_RELEASE) ->then ->integer($result) ->isEqualTo(LOCK_UN) ->when($result = SUT::LOCK_NO_BLOCK) ->then ->integer($result) ->isEqualTo(LOCK_NB); } } when($result = new \Mock\Hoa\Stream\IStream\Out()) ->then ->object($result) ->isInstanceOf(SUT::class) ->isInstanceOf(LUT\IStream\Stream::class); } } when($result = new \Mock\Hoa\Stream\IStream\Pathable()) ->then ->object($result) ->isInstanceOf(SUT::class) ->isInstanceOf(LUT\IStream\Stream::class); } } when($result = new \Mock\Hoa\Stream\IStream\Pointable()) ->then ->object($result) ->isInstanceOf(SUT::class) ->isInstanceOf(LUT\IStream\Stream::class); } public function case_constants() { $this ->when($result = SUT::SEEK_SET) ->then ->integer($result) ->isEqualTo(SEEK_SET) ->when($result = SUT::SEEK_CURRENT) ->then ->integer($result) ->isEqualTo(SEEK_CUR) ->when($result = SUT::SEEK_END) ->then ->integer($result) ->isEqualTo(SEEK_END); } } when($result = new \Mock\Hoa\Stream\IStream\Statable()) ->then ->object($result) ->isInstanceOf(SUT::class) ->isInstanceOf(LUT\IStream\Stream::class); } public function case_constants() { $this ->when($result = SUT::SIZE_UNDEFINED) ->then ->integer($result) ->isEqualTo(-1); } } when($result = new \Mock\Hoa\Stream\IStream\Stream()) ->then ->object($result) ->isInstanceOf(SUT::class); } } when($result = new \Mock\Hoa\Stream\IStream\Structural()) ->then ->object($result) ->isInstanceOf(SUT::class) ->isInstanceOf(LUT\IStream\Stream::class); } } when($result = new \Mock\Hoa\Stream\IStream\Touchable()) ->then ->object($result) ->isInstanceOf(SUT::class) ->isInstanceOf(LUT\IStream\Stream::class); } public function case_constants() { $this ->when($result = SUT::OVERWRITE) ->then ->boolean($result) ->isEqualTo(true) ->when($result = SUT::DO_NOT_OVERWRITE) ->then ->boolean($result) ->isEqualTo(false) ->when($result = SUT::MAKE_DIRECTORY) ->then ->boolean($result) ->isEqualTo(true) ->when($result = SUT::DO_NOT_MAKE_DIRECTORY) ->then ->boolean($result) ->isEqualTo(false); } } when($result = new SUT(__FILE__)) ->then ->object($result) ->isInstanceOf(LUT\IStream\Stream::class) ->isInstanceOf(Event\Listenable::class); } public function case_constants() { $this ->integer(SUT::NAME) ->isEqualTo(0) ->integer(SUT::HANDLER) ->isEqualTo(1) ->integer(SUT::RESOURCE) ->isEqualTo(2) ->integer(SUT::CONTEXT) ->isEqualTo(3); } public function case_construct() { $this ->given($name = __FILE__) ->when($result = new SUT($name)) ->then ->string($result->getStreamName()) ->isEqualTo($name) ->boolean($this->invoke($result)->hasBeenDeferred()) ->isFalse() ->let($listener = $this->invoke($result)->getListener()) ->object($listener) ->isInstanceOf(Event\Listener::class) ->boolean($listener->listenerExists('authrequire')) ->isTrue() ->boolean($listener->listenerExists('authresult')) ->isTrue() ->boolean($listener->listenerExists('complete')) ->isTrue() ->boolean($listener->listenerExists('connect')) ->isTrue() ->boolean($listener->listenerExists('failure')) ->isTrue() ->boolean($listener->listenerExists('mimetype')) ->isTrue() ->boolean($listener->listenerExists('progress')) ->isTrue() ->boolean($listener->listenerExists('redirect')) ->isTrue() ->boolean($listener->listenerExists('resolve')) ->isTrue() ->boolean($listener->listenerExists('size')) ->isTrue() ->boolean(Event::eventExists('hoa://Event/Stream/' . $name)) ->isTrue() ->boolean(Event::eventExists('hoa://Event/Stream/' . $name . ':close-before')) ->isTrue(); } public function case_construct_with_a_context() { $this ->given( $name = __FILE__, $contextName = 'foo', LUT\Context::getInstance($contextName) ) ->when($result = new SUT($name, $contextName)) ->then ->string($result->getStreamName()) ->isEqualTo($name) ->boolean($this->invoke($result)->hasBeenDeferred()) ->isFalse() ->object($this->invoke($result)->getListener()) ->isInstanceOf(Event\Listener::class); } public function case_construct_with_deferred_opening() { $this ->given($name = __FILE__) ->when($result = new SUT($name, null, true)) ->then ->boolean($this->invoke($result)->hasBeenDeferred()) ->isTrue() ->boolean($result->isOpened()) ->isFalse() ->variable($result->getStreamName()) ->isNull(); } public function case_open() { $this ->given( $name = __FILE__, $stream = new SUT($name, null, true) ) ->when($result = $stream->open()) ->then ->object($result) ->isIdenticalTo($stream) ->boolean($this->invoke($result)->hasBeenDeferred()) ->isTrue() ->boolean($result->isOpened()) ->isTrue() ->string($result->getStreamName()) ->isEqualTo($name) ->integer($result->getStreamBufferSize()) ->isEqualTo(SUT::DEFAULT_BUFFER_SIZE); } public function case_close() { $this ->given( $name = __FILE__, $stream = new SUT($name), $resource = $stream->getStream(), $context = $stream->getStreamContext() ) ->when($result = $stream->close()) ->then ->variable($result) ->isNull() ->boolean($stream->isOpened()) ->isFalse() ->variable(SUT::getStreamHandler($stream)) ->isNull() ->variable($stream->getStreamName()) ->isEqualTo($name) ->variable($stream->getStream()) ->isEqualTo($resource) ->variable($stream->getStreamContext()) ->isEqualTo($context) ->boolean(Event::eventExists('hoa://Event/Stream/' . $name)) ->isFalse() ->boolean(Event::eventExists('hoa://Event/Stream/' . $name . ':close-before')) ->isFalse(); } public function case_close_more_than_once() { $this ->given( $name = __FILE__, $stream = new SUT($name), $close1 = $stream->close() ) ->when($result = $stream->close()) ->then ->variable($result) ->isIdenticalTo($close1); } public function case_open_close_open() { $this ->given( $name = __FILE__, $stream = new SUT($name, null, true), $stream->open(), $resource = $stream->getStream(), $context = $stream->getStreamContext(), $handler = SUT::getStreamHandler($stream), $this->function->stream_set_write_buffer = 0, $stream->setStreamBuffer(42), $stream->close() ) ->when($result = $stream->open()) ->then ->string($result->getStreamName()) ->isEqualTo($name) ->resource($result->getStream()) ->isNotEqualTo($resource) ->object($handler) ->isIdenticalTo($result) ->object($this->invoke($stream)->getListener()) ->isInstanceOf(Event\Listener::class) ->boolean(Event::eventExists('hoa://Event/Stream/' . $name)) ->isTrue() ->boolean(Event::eventExists('hoa://Event/Stream/' . $name . ':close-before')) ->isTrue() ->integer($stream->getStreamBufferSize()) ->isEqualTo(SUT::DEFAULT_BUFFER_SIZE); } public function case_close_event_close_before() { $self = $this; $this ->given( $name = 'hoa://Test/Vfs/Foo?type=file', $stream = new SUT($name), Event::getEvent('hoa://Event/Stream/' . $name . ':close-before')->attach( function (Event\Bucket $bucket) use ($self, &$called) { $called = true; $self ->variable($bucket->getData()) ->isNull() ->boolean($bucket->getSource()->isOpened()) ->isTrue(); } ) ) ->when($result = $stream->close()) ->then ->boolean($called) ->isTrue(); } public function case_get_stream_name() { $this ->given( $name = __FILE__, $stream = new SUT($name) ) ->when($result = $stream->getStreamName()) ->then ->string($result) ->isEqualTo($name); } public function case_get_stream() { $this ->given( $name = __FILE__, $stream = new SUT($name) ) ->when($result = $stream->getStream()) ->then ->resource($result) ->isStream($name); } public function case_get_stream_context() { $this ->given( $name = __FILE__, $contextName = 'foo', $context = LUT\Context::getInstance($contextName), $stream = new SUT($name, $contextName) ) ->when($result = $stream->getStreamContext()) ->then ->object($result) ->isIdenticalTo($context); } public function case_get_stream_context_with_no_context_given() { $this ->given( $name = __FILE__, $stream = new SUT($name) ) ->when($result = $stream->getStreamContext()) ->then ->variable($result) ->isNull(); } public function case_get_stream_handler() { $this ->given( $name = __FILE__, $stream = new SUT($name) ) ->when($result = SUT::getStreamHandler($name)) ->then ->object($result) ->isIdenticalTo($result); } public function case_get_stream_handler_of_unknown_stream() { $this ->when($result = SUT::getStreamHandler('foo')) ->then ->variable($result) ->isNull(); } public function case__set_stream() { $this ->given( $stream = new SUT(__FILE__), $oldStream = $stream->getStream(), $newStream = fopen('php://memory', 'rb') ) ->when($result = $stream->_setStream($newStream)) ->then ->resource($result) ->isIdenticalTo($oldStream) ->isStream() ->resource($stream->getStream()) ->isStream() ->isIdenticalTo($newStream); } public function case__set_stream_invalid_resource() { $this ->given($stream = new SUT(__FILE__)) ->exception(function () use ($stream) { $stream->_setStream(true); }) ->isInstanceOf(LUT\Exception::class); } public function case__set_stream_unknown_resource() { $this ->given( $stream = new SUT(__FILE__), $oldStream = $stream->getStream(), $newStream = fopen('php://memory', 'rb'), $this->function->is_resource = false, $this->function->gettype = 'resource', $this->function->get_resource_type = 'Unknown' ) ->when($result = $stream->_setStream($newStream)) ->then ->resource($result) ->isIdenticalTo($oldStream) ->isStream() ->resource($stream->getStream()) ->isStream() ->isIdenticalTo($newStream); } public function case_is_opened() { $this ->given($stream = new SUT(__FILE__)) ->when($result = $stream->isOpened()) ->then ->boolean($result) ->isTrue(); } public function case_is_not_opened() { $this ->given($stream = new SUT(__FILE__, null, true)) ->when($result = $stream->isOpened()) ->then ->boolean($result) ->isFalse() ->when( $stream->open(), $result = $stream->isOpened() ) ->then ->boolean($result) ->isTrue(); } public function case_set_stream_timeout() { $self = $this; $this ->given( $stream = new SUT(__FILE__), $this->function->stream_set_timeout = function ($_stream, $_seconds, $_microseconds) use ($self, $stream, &$called) { $called = true; $self ->resource($_stream) ->isIdenticalTo($stream->getStream()) ->integer($_seconds) ->isEqualTo(7) ->integer($_microseconds) ->isEqualTo(42); return true; } ) ->when($result = $stream->setStreamTimeout(7, 42)) ->then ->boolean($result) ->isTrue() ->boolean($called) ->isTrue(); } public function case_has_been_deferred() { $this ->given($stream = new SUT(__FILE__, null, true)) ->when($result = $this->invoke($stream)->hasBeenDeferred()) ->then ->boolean($result) ->isTrue(); } public function case_has_not_been_deferred() { $this ->given($stream = new SUT(__FILE__)) ->when($result = $this->invoke($stream)->hasBeenDeferred()) ->then ->boolean($result) ->isFalse(); } public function case_has_timed_out() { $this ->given( $stream = new SUT(__FILE__), $this->function->stream_get_meta_data = [ 'timed_out' => true ] ) ->when($result = $stream->hasTimedOut()) ->then ->boolean($result) ->isTrue(); } public function case_has_not_timed_out() { $this ->given( $stream = new SUT(__FILE__), $this->function->stream_get_meta_data = [ 'timed_out' => false ] ) ->when($result = $stream->hasTimedOut()) ->then ->boolean($result) ->isFalse(); } public function case_set_stream_blocking() { $self = $this; $this ->given( $stream = new SUT(__FILE__), $this->function->stream_set_blocking = function ($_stream, $_mode) use ($self, $stream, &$called) { $called = true; $self ->resource($_stream) ->isIdenticalTo($stream->getStream()) ->integer($_mode) ->isEqualTo(1); return true; } ) ->when($result = $stream->setStreamBlocking(true)) ->then ->boolean($result) ->isTrue() ->boolean($called) ->isTrue(); } public function case_get_default_stream_buffer_size() { $self = $this; $this ->given($stream = new SUT(__FILE__)) ->when($result = $stream->getStreamBufferSize()) ->then ->integer($result) ->isEqualTo(8192); } public function case_set_stream_buffer() { $self = $this; $this ->given( $stream = new SUT(__FILE__), $this->function->stream_set_write_buffer = function ($_stream, $_buffer) use ($self, $stream, &$called) { $called = true; $self ->resource($_stream) ->isIdenticalTo($stream->getStream()) ->integer($_buffer) ->isEqualTo(42); return 0; } ) ->when($result = $stream->setStreamBuffer(42)) ->then ->boolean($result) ->isTrue() ->boolean($called) ->isTrue() ->integer($stream->getStreamBufferSize()) ->isEqualTo(42); } public function case_set_stream_buffer_fail() { $self = $this; $this ->given( $stream = new SUT(__FILE__), $oldStreamBufferSize = $stream->getStreamBufferSize(), $this->function->stream_set_write_buffer = function ($_stream, $_buffer) use ($self, $stream, &$called) { $called = true; $self ->resource($_stream) ->isIdenticalTo($stream->getStream()) ->integer($_buffer) ->isEqualTo(42); return 1; } ) ->when($result = $stream->setStreamBuffer(42)) ->then ->boolean($result) ->isFalse() ->boolean($called) ->isTrue() ->integer($stream->getStreamBufferSize()) ->isEqualTo($oldStreamBufferSize); } public function case_disable_stream_buffer() { $self = $this; $this ->given( $stream = new SUT(__FILE__), $this->function->stream_set_write_buffer = function ($_stream, $_buffer) use ($self, $stream, &$called) { $called = true; $self ->resource($_stream) ->isIdenticalTo($stream->getStream()) ->integer($_buffer) ->isEqualTo(0); return 0; } ) ->when($result = $stream->disableStreamBuffer()) ->then ->boolean($result) ->isTrue() ->boolean($called) ->isTrue() ->integer($stream->getStreamBufferSize()) ->isEqualTo(0); } public function case_get_stream_wrapper_name_with_no_wrapper() { $this ->given($stream = new SUT(__FILE__)) ->when($result = $stream->getStreamWrapperName()) ->then ->string($result) ->isEqualTo('file'); } public function case_get_stream_wrapper_name() { $this ->given($stream = new SUT('hoa://Test/Vfs/Foo?type=file')) ->when($result = $stream->getStreamWrapperName()) ->then ->string($result) ->isEqualTo('hoa'); } public function case_get_stream_meta_data() { $this ->given($stream = new SUT(__FILE__)) ->when($result = $stream->getStreamMetaData()) ->then ->array($result) ->isEqualTo([ 'timed_out' => false, 'blocked' => true, 'eof' => false, 'wrapper_type' => 'plainfile', 'stream_type' => 'STDIO', 'mode' => 'rb', 'unread_bytes' => 0, 'seekable' => true, 'uri' => __FILE__ ]); } public function case_is_borrowing() { $this ->given( $streamA1 = new SUT(__FILE__), $streamA2 = new SUT(__FILE__) ) ->when($result = $streamA2->isBorrowing()) ->then ->boolean($result) ->isTrue() ->boolean($streamA1->isBorrowing()) ->isFalse(); } public function case_is_not_borrowing() { $this ->given($stream = new SUT(__FILE__)) ->when($result = $stream->isBorrowing()) ->then ->boolean($result) ->isFalse(); } public function case_shutdown_destructor() { $this ->given( $stream = new \Mock\Hoa\Stream\Test\Unit\SUTWithPublicClose(__FILE__), $this->calling($stream)->_close = function () use (&$called) { $called = true; } ) ->when($result = SUT::_Hoa_Stream()) ->then ->boolean($called) ->isTrue(); } public function case_destruct_an_opened_stream() { $this ->given( $stream = new \Mock\Hoa\Stream\Test\Unit\SUTWithPublicClose(__FILE__), $this->calling($stream)->_close = function () use (&$called) { $called = true; } ) ->when($result = $stream->__destruct()) ->then ->boolean($called) ->isTrue(); } public function case_destruct_a_deferred_stream() { $this ->given( $stream = new \Mock\Hoa\Stream\Test\Unit\SUTWithPublicClose(__FILE__, null, true), $this->calling($stream)->_close = function () use (&$called) { $called = true; } ) ->when($result = $stream->__destruct()) ->then ->variable($called) ->isNull(); } public function case_protocol_reach_id() { $this ->given( $name = 'hoa://Test/Vfs/Foo?type=file', $stream = new SUT($name) ) ->when($result = resolve('hoa://Library/Stream#' . $name)) ->then ->object($result) ->isIdenticalTo($stream); } public function case_protocol_reach_unknown_id() { $this ->given($name = 'hoa://Test/Vfs/Foo?type=file') ->when($result = resolve('hoa://Library/Stream#' . $name)) ->then ->variable($result) ->isNull(); } } class SUT extends LUT\Stream { protected function &_open($streamName, LUT\Context $context = null) { if (null === $context) { $out = fopen($streamName, 'rb'); } else { $out = fopen($streamName, 'rb', false, $context->getContext()); } return $out; } protected function _close() { return fclose($this->getStream()); } } class SUTWithPublicClose extends SUT { public function _close() { return parent::_close(); } } when($result = new SUT('foo', 0)) ->then ->object($result) ->isInstanceOf(LUT\Exception::class); } } when($result = new \Mock\Hoa\Stream\Wrapper\IWrapper\File()) ->then ->object($result) ->isInstanceOf(SUT::class); } } when($result = new \Mock\Hoa\Stream\Wrapper\IWrapper\IWrapper()) ->then ->object($result) ->isInstanceOf(SUT::class) ->isInstanceOf(LUT\Wrapper\IWrapper\File::class) ->isInstanceOf(LUT\Wrapper\IWrapper\Stream::class); } } when($result = new \Mock\Hoa\Stream\Wrapper\IWrapper\Stream()) ->then ->object($result) ->isInstanceOf(SUT::class); } } given($oldIsRegistered = SUT::isRegistered('foo')) ->when($result = SUT::register('foo', 'StdClass')) ->then ->boolean($result) ->isTrue() ->boolean($oldIsRegistered) ->isFalse() ->boolean(SUT::isRegistered('foo')) ->isTrue(); } public function case_register_already_registered() { $this ->exception(function () { SUT::register('php', 'ClassName'); }) ->isInstanceOf(LUT\Exception::class) ->hasMessage('The protocol php is already registered.'); } public function case_register_implementation_does_not_exist() { $this ->exception(function () { SUT::register('foo', 'ClassName'); }) ->isInstanceOf(LUT\Exception::class) ->hasMessage( 'Cannot use the ClassName class for the implementation ' . 'of the foo protocol because it is not found.' ); } public function case_unregister() { $this ->given( SUT::register('foo', 'StdClass'), $oldIsRegistered = SUT::isRegistered('foo') ) ->when($result = SUT::unregister('foo')) ->then ->boolean($result) ->isTrue() ->boolean($oldIsRegistered) ->isTrue() ->boolean(SUT::isRegistered('foo')) ->isFalse(); } public function case_unregister_unregistered_protocol() { $this ->when($result = SUT::unregister('foo')) ->then ->boolean($result) ->isFalse(); } public function case_restore_registered_protocol() { $this ->when($result = SUT::restore('php')) ->then ->boolean($result) ->isTrue(); } public function case_restore_unregistered_protocol() { $this ->when($result = SUT::restore('foo')) ->then ->boolean($result) ->isFalse(); } public function case_is_registered() { $this ->when($result = SUT::isRegistered('php')) ->then ->boolean($result) ->isTrue(); } public function case_is_not_registered() { $this ->when($result = SUT::isRegistered('foo')) ->then ->boolean($result) ->isFalse(); } public function case_get_registered() { $this ->when($result = SUT::getRegistered()) ->then ->array($result) ->containsValues([ 'https', 'php', 'file', 'glob', 'data', 'http', 'hoa' ]); } public function case_get_registered_dynamically() { $this ->given($oldCount = count(SUT::getRegistered())) ->when( SUT::register('foo', \StdClass::class), $result = SUT::getRegistered() ) ->then ->integer(count($result)) ->isEqualTo($oldCount + 1) ->when( SUT::unregister('foo'), $result = SUT::getRegistered() ) ->then ->integer(count($result)) ->isEqualTo($oldCount); } } getOption($v)) { switch ($c) { case 'b': $base = intval($v); break; case '__ambiguous': $this->resolveOptionAmbiguity($v); break; case 'h': case '?': default: return $this->usage(); } } $this->parser->listInputs($code); $char = Ustring::fromCode(base_convert($code, $base, 10)); echo $char; return; } public function usage() { echo 'Usage : ustring:fromcode ', "\n", 'Options :', "\n", $this->makeUsageOptionsList([ 'b' => 'Specify the base of the code (16 by default).', 'help' => 'This help.' ]), "\n"; return; } } __halt_compiler(); Get a character from its code. getOption($v)) { switch ($c) { case 'b': $base = intval($v); break; case '__ambiguous': $this->resolveOptionAmbiguity($v); break; case 'h': case '?': default: return $this->usage(); } } $this->parser->listInputs($char); $code = base_convert((string) Ustring::toCode($char), 10, $base); echo $code, "\n"; return; } public function usage() { echo 'Usage : ustring:tocode ', "\n", 'Options :', "\n", $this->makeUsageOptionsList([ 'b' => 'Get the code in a specific base (16 by default).', 'help' => 'This help.' ]), "\n"; return; } } __halt_compiler(); Transform a character into its code. array_fill(-1, $n - $m + $k + 3, -2)]; for ($q = 0, $max = $k - 1; $q <= $max; ++$q) { $L[$q][-$q - 1] = $L[$q][-$q - 2] = $q - 1; } for ($q = 0; $q <= $k; ++$q) { for ($d = -$q, $max = $n - $m + $k - $q; $d <= $max; ++$d) { $l = min( max( $L[$q - 1][$d - 1], $L[$q - 1][$d ] + 1, $L[$q - 1][$d + 1] + 1 ), $m - 1 ); $a = substr($x, $l + 1, $m - $l); $b = substr($y, $l + 1 + $d, $n - $l - $d); $L[$q][$d] = $l + static::lcp($a, $b); if ($L[$q][$d] == $m - 1 || $d + $L[$q][$d] == $n - 1) { $j = $m + $d; $i = max(0, $j - $m); $offset[$q][] = ['i' => $i, 'j' => $j, 'l' => $j - $i]; } } } return empty($offset) ? $offset : $offset[$k]; } public static function lcp($x, $y) { $max = min(strlen($x), strlen($y)); $i = 0; while ($i < $max && $x[$i] == $y[$i]) { ++$i; } return $i; } } when($result = LUT::toCode(chr(160))) ->then ->integer($result) ->isEqualTo(0xa0); } } given( $x = 'GATAA', $y = 'CAGATAAGAGAA', $k = 1 ) ->when($result = LUT\Search::approximated($y, $x, $k)) ->then ->array($result) ->isEqualTo([ 0 => [ 'i' => 1, 'j' => 6, 'l' => 5 ], 1 => [ 'i' => 2, 'j' => 7, 'l' => 5 ], 2 => [ 'i' => 3, 'j' => 8, 'l' => 5 ], 3 => [ 'i' => 7, 'j' => 12, 'l' => 5 ] ]); } } given($this->function->function_exists = true) ->then ->boolean(LUT::checkMbString()) ->isTrue(); } public function case_check_no_mbstring() { $this ->given( $this->function->function_exists = function ($name) { return 'mb_substr' !== $name; } ) ->exception(function () { new LUT(); }) ->isInstanceOf('Hoa\Ustring\Exception'); } public function case_append_ltr() { $this ->given($string = new LUT('je')) ->when($result = $string->append(' t\'aime')) ->then ->object($result) ->isIdenticalTo($string) ->string((string) $result) ->isEqualTo('je t\'aime'); } public function case_append_rtl() { $this ->given($string = new LUT('أ')) ->when($result = $string->append('حبك')) ->then ->object($result) ->isIdenticalTo($string) ->string((string) $result) ->isEqualTo('أحبك'); } public function case_prepend_ltr() { $this ->given($string = new LUT(' t\'aime')) ->when($result = $string->prepend('je')) ->then ->object($result) ->isIdenticalTo($string) ->string((string) $result) ->isEqualTo('je t\'aime'); } public function case_prepend_rtl() { $this ->given($string = new LUT('ك')) ->when($result = $string->prepend('أحب')) ->then ->object($result) ->isIdenticalTo($string) ->string((string) $result) ->isEqualTo('أحبك'); } public function case_pad_beginning_ltr() { $this ->given($string = new LUT('je t\'aime')) ->when($result = $string->pad(20, '👍 💩 😄 ❤️ ', LUT::BEGINNING)) ->then ->object($result) ->isIdenticalTo($string) ->string((string) $result) ->isEqualTo('👍 💩 😄 ❤️ 👍 je t\'aime'); } public function case_pad_beginning_rtl() { $this ->given($string = new LUT('أحبك')) ->when($result = $string->pad(20, '👍 💩 😄 ❤️ ', LUT::BEGINNING)) ->then ->object($result) ->isIdenticalTo($string) ->string((string) $result) ->isEqualTo('👍 💩 😄 ❤️ 👍 💩 😄 ❤أحبك'); } public function case_pad_end_ltr() { $this ->given($string = new LUT('je t\'aime')) ->when($result = $string->pad(20, '👍 💩 😄 ❤️ ', LUT::END)) ->then ->object($result) ->isIdenticalTo($string) ->string((string) $result) ->isEqualTo('je t\'aime👍 💩 😄 ❤️ 👍 '); } public function case_pad_end_rtl() { $this ->given($string = new LUT('أحبك')) ->when($result = $string->pad(20, '👍 💩 😄 ❤️ ', LUT::END)) ->then ->object($result) ->isIdenticalTo($string) ->string((string) $result) ->isEqualTo('أحبك👍 💩 😄 ❤️ 👍 💩 😄 ❤'); } public function case_compare_no_collator() { $this ->given( $this->function->class_exists = function ($name) { return 'Collator' !== $name; }, $string = new LUT('b') ) ->case_compare(); } public function case_compare() { $this ->given($string = new LUT('b')) ->when($result = $string->compare('a')) ->then ->integer($result) ->isEqualTo(1) ->when($result = $string->compare('b')) ->then ->integer($result) ->isEqualTo(0) ->when($result = $string->compare('c')) ->then ->integer($result) ->isEqualTo(-1); } public function case_collator() { $this ->given( $this->function->setlocale = 'fr_FR', $collator = LUT::getCollator() ) ->when($result = $collator->getLocale(\Locale::VALID_LOCALE)) ->then ->string($result) ->isEqualTo('fr'); } public function case_safe_unsafe_pattern() { $this ->given($pattern = '/foo/i') ->when($result = LUT::safePattern($pattern)) ->then ->string($result) ->isEqualto('/foo/iu'); } public function case_safe_safe_pattern() { $this ->given($pattern = '/foo/ui') ->when($result = LUT::safePattern($pattern)) ->then ->string($result) ->isEqualto('/foo/ui'); } public function case_match_default() { $this ->given( $pattern = '/💩/u', $string = new LUT('foo 💩 bar') ) ->when($result = $string->match($pattern, $matches)) ->then ->integer($result) ->isEqualTo(1) ->array($matches) ->isEqualTo([ 0 => '💩' ]); } public function case_match_offset() { $this ->given( $pattern = '/💩/u', $string = new LUT('foo 💩 bar') ) ->when($result = $string->match($pattern, $matches, 0, 0)) ->then ->integer($result) ->isEqualTo(1) ->array($matches) ->isEqualTo([0 => '💩']) ->when($result = $string->match($pattern, $matches, 0, 4)) ->then ->integer($result) ->isEqualTo(1) ->array($matches) ->isEqualTo([0 => '💩']) ->when($result = $string->match($pattern, $matches, 0, 5)) ->then ->integer($result) ->isEqualTo(0) ->array($matches) ->isEmpty(); } public function case_match_with_offset() { $this ->given( $pattern = '/💩/u', $string = new LUT('foo 💩 bar') ) ->when($result = $string->match($pattern, $matches, $string::WITH_OFFSET)) ->then ->integer($result) ->isEqualTo(1) ->array($matches) ->isEqualTo([ 0 => [ 0 => '💩', 1 => 4 ] ]); } public function case_match_all_default() { $this ->given( $pattern = '/💩/u', $string = new LUT('foo 💩 bar 💩 baz') ) ->when($result = $string->match($pattern, $matches, 0, 0, true)) ->then ->integer($result) ->isEqualTo(2) ->array($matches) ->isEqualTo([ 0 => [ 0 => '💩', 1 => '💩' ] ]); } public function case_match_all_with_offset() { $this ->given( $pattern = '/💩/u', $string = new LUT('foo 💩 bar 💩 baz') ) ->when($result = $string->match($pattern, $matches, $string::WITH_OFFSET, 0, true)) ->then ->integer($result) ->isEqualTo(2) ->array($matches) ->isEqualTo([ 0 => [ 0 => [ 0 => '💩', 1 => 4 ], 1 => [ 0 => '💩', 1 => 13 ] ] ]); } public function case_match_all_grouped_by_pattern() { $this ->given( $pattern = '/(💩)/u', $string = new LUT('foo 💩 bar 💩 baz') ) ->when($result = $string->match($pattern, $matches, $string::GROUP_BY_PATTERN, 0, true)) ->then ->integer($result) ->isEqualTo(2) ->array($matches) ->isEqualTo([ 0 => [ 0 => '💩', 1 => '💩' ], 1 => [ 0 => '💩', 1 => '💩' ] ]); } public function case_match_all_grouped_by_tuple() { $this ->given( $pattern = '/(💩)/u', $string = new LUT('foo 💩 bar 💩 baz') ) ->when($result = $string->match($pattern, $matches, $string::GROUP_BY_TUPLE, 0, true)) ->then ->integer($result) ->isEqualTo(2) ->array($matches) ->isEqualTo([ 0 => [ 0 => '💩', 1 => '💩' ], 1 => [ 0 => '💩', 1 => '💩' ] ]); } public function case_replace() { $this ->given($string = new LUT('❤️ 💩 💩')) ->when($result = $string->replace('/💩/u', '😄')) ->then ->object($result) ->isIdenticalTo($string) ->string((string) $result) ->isEqualTo('❤️ 😄 😄'); } public function case_replace_limited() { $this ->given($string = new LUT('❤️ 💩 💩')) ->when($result = $string->replace('/💩/u', '😄', 1)) ->then ->object($result) ->isIdenticalTo($string) ->string((string) $result) ->isEqualTo('❤️ 😄 💩'); } public function case_split_default() { $this ->given($string = new LUT('❤️💩❤️💩❤️')) ->when($result = $string->split('/💩/')) ->then ->array($result) ->isEqualTo([ 0 => '❤️', 1 => '❤️', 2 => '❤️' ]); } public function case_split_default_limited() { $this ->given($string = new LUT('❤️💩❤️💩❤️')) ->when($result = $string->split('/💩/', 1)) ->then ->array($result) ->isEqualTo([ 0 => '❤️💩❤️💩❤️' ]); } public function case_split_with_delimiters() { $this ->given($string = new LUT('❤️💩❤️💩❤️')) ->when($result = $string->split('/💩/', -1, $string::WITH_DELIMITERS)) ->then ->array($result) ->isEqualTo([ 0 => '❤️', 1 => '❤️', 2 => '❤️' ]); } public function case_split_with_offset() { $this ->given($string = new LUT('❤️💩❤️💩❤️')) ->when($result = $string->split('/💩/', -1, $string::WITH_OFFSET)) ->then ->array($result) ->isEqualTo([ 0 => [ 0 => '❤️', 1 => 0 ], 1 => [ 0 => '❤️', 1 => 10 ], 2 => [ 0 => '❤️', 1 => 20 ] ]); } public function case_iterator_ltr() { $this ->given($string = new LUT('je t\'aime')) ->when($result = iterator_to_array($string)) ->then ->array($result) ->isEqualTo([ 'j', 'e', ' ', 't', '\'', 'a', 'i', 'm', 'e' ]); } public function case_iterator_rtl() { $this ->given($string = new LUT('أحبك')) ->when($result = iterator_to_array($string)) ->then ->array($result) ->isEqualTo([ 'أ', 'ح', 'ب', 'ك' ]); } public function case_to_lower() { $this ->given($string = new LUT('Σ \'ΑΓΑΠΏ')) ->when($result = $string->toLowerCase()) ->then ->object($result) ->isIdenticalTo($string) ->string((string) $result) ->isEqualTo('σ \'αγαπώ') ->given($string = new LUT('JE T\'AIME')) ->when($result = $string->toLowerCase()) ->then ->object($result) ->isIdenticalTo($string) ->string((string) $result) ->isEqualTo('je t\'aime'); } public function case_to_upper() { $this ->given($string = new LUT('σ \'αγαπώ')) ->when($result = $string->toUpperCase()) ->then ->object($result) ->isIdenticalTo($string) ->string((string) $result) ->isEqualTo('Σ \'ΑΓΑΠΏ') ->given($string = new LUT('je t\'aime')) ->when($result = $string->toUpperCase()) ->then ->object($result) ->isIdenticalTo($string) ->string((string) $result) ->isEqualTo('JE T\'AIME'); } public function case_trim_default() { $this ->given($string = new LUT('💩💩❤️💩💩')) ->when($result = $string->trim('💩')) ->then ->object($result) ->isIdenticalTo($string) ->string((string) $result) ->isEqualTo('❤️'); } public function case_trim_beginning() { $this ->given($string = new LUT('💩💩❤️💩💩')) ->when($result = $string->trim('💩', $string::BEGINNING)) ->then ->object($result) ->isIdenticalTo($string) ->string((string) $result) ->isEqualTo('❤️💩💩'); } public function case_trim_end() { $this ->given($string = new LUT('💩💩❤️💩💩')) ->when($result = $string->trim('💩', $string::END)) ->then ->object($result) ->isIdenticalTo($string) ->string((string) $result) ->isEqualTo('💩💩❤️'); } public function case_offset_get_ltr() { $this ->given($string = new LUT('je t\'aime')) ->when($result = $string[0]) ->then ->string($result) ->isEqualTo('j') ->when($result = $string[-1]) ->then ->string($result) ->isEqualTo('e'); } public function case_offset_get_rtl() { $this ->given($string = new LUT('أحبك')) ->when($result = $string[0]) ->then ->string($result) ->isEqualTo('أ') ->when($result = $string[-1]) ->then ->string($result) ->isEqualTo('ك'); } public function case_offset_set() { $this ->given($string = new LUT('أحبﻙ')) ->when($string[-1] = 'ك') ->then ->string((string) $string) ->isEqualTo('أحبك'); } public function case_offset_unset() { $this ->given($string = new LUT('أحبك😄')) ->when(function () use ($string) { unset($string[-1]); }) ->then ->string((string) $string) ->isEqualTo('أحبك'); } public function case_reduce() { $this ->given($string = new LUT('أحبك')) ->when($result = $string->reduce(0, 1)) ->then ->object($result) ->isIdenticalTo($string) ->string((string) $result) ->isEqualTo('أ'); } public function case_count() { $this ->given($string = new LUT('je t\'aime')) ->when($result = count($string)) ->then ->integer($result) ->isEqualTo(9) ->given($string = new LUT('أحبك')) ->when($result = count($string)) ->then ->integer($result) ->isEqualTo(4) ->given($string = new LUT('💩')) ->when($result = count($string)) ->then ->integer($result) ->isEqualTo(1); } public function case_byte_at() { $this ->given($string = new LUT('💩')) ->when($result = $string->getByteAt(0)) ->then ->integer(ord($result)) ->isEqualTo(0xf0) ->when($result = $string->getByteAt(1)) ->then ->integer(ord($result)) ->isEqualTo(0x9f) ->when($result = $string->getByteAt(2)) ->then ->integer(ord($result)) ->isEqualTo(0x92) ->when($result = $string->getByteAt(3)) ->then ->integer(ord($result)) ->isEqualTo(0xa9) ->when($result = $string->getByteAt(-1)) ->then ->integer(ord($result)) ->isEqualTo(0xa9); } public function case_bytes_length() { $this ->given($string = new LUT('💩')) ->when($result = $string->getBytesLength()) ->then ->integer($result) ->isEqualTo(4); } public function case_get_width() { $this ->given($string = new LUT('💩')) ->when($result = $string->getWidth()) ->then ->integer($result) ->isEqualTo(1) ->given($string = new LUT('習')) ->when($result = $string->getWidth()) ->then ->integer($result) ->isEqualTo(2); } public function case_get_char_direction() { $this ->when($result = LUT::getCharDirection('A')) ->then ->integer($result) ->isEqualTo(LUT::LTR) ->when($result = LUT::getCharDirection('ا')) ->then ->integer($result) ->isEqualTo(LUT::RTL); } public function case_get_char_width() { $this ->given( $data = [ [0x0, 0], [0x19, -1], [0x7f, -1], [0x9f, -1], [0xa0, 1], [0x300, 0], [0x488, 0], [0x600, 0], [0xad, 1], [0x1160, 0], [0x11ff, 0], [0x200b, 0], [0x1100, 2], [0x2160, 1], [0x3f60, 2], [0x303f, 1], [0x2329, 2], [0xaed0, 2], [0x232a, 2], [0xffa4, 1], [0xfe10, 2], [0xfe30, 2], [0xff00, 2], [0xf900, 2] ] ) ->when(function () use ($data) { foreach ($data as $datum) { list($code, $width) = $datum; $this ->when($result = LUT::getCharWidth(LUT::fromCode($code))) ->then ->integer($result) ->isEqualTo($width); } }); } public function case_is_char_printable() { $this ->when($result = LUT::isCharPrintable(LUT::fromCode(0x7f))) ->then ->boolean($result) ->isFalse() ->when($result = LUT::isCharPrintable(LUT::fromCode(0xa0))) ->then ->boolean($result) ->isTrue() ->when($result = LUT::isCharPrintable(LUT::fromCode(0x1100))) ->then ->boolean($result) ->isTrue(); } public function case_from_code() { $this ->when($result = LUT::fromCode(0x7e)) ->then ->string($result) ->isEqualTo('~') ->when($result = LUT::fromCode(0xa7)) ->then ->string($result) ->isEqualTo('§') ->when($result = LUT::fromCode(0x1207)) ->then ->string($result) ->isEqualTo('ሇ') ->when($result = LUT::fromCode(0x1f4a9)) ->then ->string($result) ->isEqualTo('💩'); } public function case_to_code() { $this ->when($result = LUT::toCode('~')) ->then ->integer($result) ->isEqualTo(0x7e) ->when($result = LUT::toCode('§')) ->then ->integer($result) ->isEqualTo(0xa7) ->when($result = LUT::toCode('ሇ')) ->then ->integer($result) ->isEqualTo(0x1207) ->when($result = LUT::toCode('💩')) ->then ->integer($result) ->isEqualTo(0x1f4a9); } public function case_to_binary_code() { $this ->when($result = LUT::toBinaryCode('~')) ->then ->string($result) ->isEqualTo('01111110') ->when($result = LUT::toBinaryCode('§')) ->then ->string($result) ->isEqualTo('1100001010100111') ->when($result = LUT::toBinaryCode('ሇ')) ->then ->string($result) ->isEqualTo('111000011000100010000111') ->when($result = LUT::toBinaryCode('💩')) ->then ->string($result) ->isEqualTo('11110000100111111001001010101001'); } public function case_transcode_no_iconv() { $this ->given( $this->function->function_exists = function ($name) { return 'iconv' !== $name; } ) ->exception(function () { LUT::transcode('foo', 'UTF-8'); }) ->isInstanceOf('Hoa\Ustring\Exception'); } public function case_transcode_and_isUtf8() { $this ->given($uΣ = 'Σ') ->when($Σ = LUT::transcode($uΣ, 'UTF-8', 'UTF-16')) ->then ->string($Σ) ->isNotEqualTo($uΣ) ->boolean(LUT::isUtf8($Σ)) ->isFalse() ->when($Σ = LUT::transcode($Σ, 'UTF-16', 'UTF-8')) ->string($Σ) ->isEqualTo($uΣ) ->boolean(LUT::isUtf8($Σ)) ->isTrue() ->boolean(LUT::isUtf8($uΣ)) ->isTrue(); } public function case_to_ascii_no_transliterator_no_normalizer() { $this ->given( $this->function->class_exists = function ($name) { return false === in_array($name, ['Transliterator', 'Normalizer']); }, $string = new LUT('Un été brûlant sur la côte') ) ->exception(function () use ($string) { $string->toAscii(); }) ->isInstanceOf('Hoa\Ustring\Exception'); } public function case_to_ascii_no_transliterator_no_normalizer_try() { $this ->given( $this->function->class_exists = function ($name) { return false === in_array($name, ['Transliterator', 'Normalizer']); }, $string = new LUT('Un été brûlant sur la côte') ) ->when($result = $string->toAscii(true)) ->then ->object($result) ->isIdenticalTo($string) ->string((string) $result) ->isEqualTo('Un ete brulant sur la cote'); } public function case_to_ascii_no_transliterator() { $this ->given( $this->function->class_exists = function ($name) { return 'Transliterator' !== $name; }, $string = new LUT('Un été brûlant sur la côte') ) ->when($result = $string->toAscii()) ->then ->object($result) ->isIdenticalTo($string) ->string((string) $result) ->isEqualTo('Un ete brulant sur la cote'); } public function case_to_ascii() { $this ->given( $strings = [ 'Un été brûlant sur la côte' => 'Un ete brulant sur la cote', 'Αυτή είναι μια δοκιμή' => 'Aute einai mia dokime', 'أحبك' => 'ahbk', 'キャンパス' => 'kyanpasu', 'биологическом' => 'biologiceskom', '정, 병호' => 'jeong, byeongho', 'ますだ, よしひこ' => 'masuda, yoshihiko', 'मोनिच' => 'monica', 'क्ष' => 'ksa', 'أحبك 😀' => 'ahbk (grinning face)', '∀ i ∈ ℕ' => '(for all) i (element of) N' ] ) ->when(function () use ($strings) { foreach ($strings as $original => $asciied) { $this ->given($string = new LUT($original)) ->when($result = $string->toAscii()) ->then ->object($result) ->isIdenticalTo($string) ->string((string) $result) ->isEqualTo($asciied); } }); } public function case_copy() { $this ->given($string = new LUT('foo')) ->when($result = $string->copy()) ->then ->object($result) ->isEqualTo($string); } public function case_toString() { $this ->given($datum = $this->sample($this->realdom->regex('/\w{7,42}/'))) ->when($result = new LUT($datum)) ->then ->castToString($result) ->isEqualTo($datum); } } append($string); } return; } public static function checkMbString() { return function_exists('mb_substr'); } public static function checkIconv() { return function_exists('iconv'); } public function append($substring) { $this->_string .= $substring; return $this; } public function prepend($substring) { $this->_string = $substring . $this->_string; return $this; } public function pad($length, $piece, $side = self::END) { $difference = $length - $this->count(); if (0 >= $difference) { return $this; } $handle = null; for ($i = $difference / mb_strlen($piece) - 1; $i >= 0; --$i) { $handle .= $piece; } $handle .= mb_substr($piece, 0, $difference - mb_strlen($handle)); return static::END === $side ? $this->append($handle) : $this->prepend($handle); } public function compare($string) { if (null === $collator = static::getCollator()) { return strcmp($this->_string, (string) $string); } return $collator->compare($this->_string, $string); } public static function getCollator() { if (false === class_exists('Collator')) { return null; } if (null === static::$_collator) { static::$_collator = new \Collator(setlocale(LC_COLLATE, null)); } return static::$_collator; } public static function safePattern($pattern) { $delimiter = mb_substr($pattern, 0, 1); $options = mb_substr( mb_strrchr($pattern, $delimiter, false), mb_strlen($delimiter) ); if (false === strpos($options, 'u')) { $pattern .= 'u'; } return $pattern; } public function match( $pattern, &$matches = null, $flags = 0, $offset = 0, $global = false ) { $pattern = static::safePattern($pattern); if (0 === $flags) { if (true === $global) { $flags = static::GROUP_BY_PATTERN; } } else { $flags &= ~PREG_SPLIT_OFFSET_CAPTURE; } $offset = strlen(mb_substr($this->_string, 0, $offset)); if (true === $global) { return preg_match_all( $pattern, $this->_string, $matches, $flags, $offset ); } return preg_match($pattern, $this->_string, $matches, $flags, $offset); } public function replace($pattern, $replacement, $limit = -1) { $pattern = static::safePattern($pattern); if (false === is_callable($replacement)) { $this->_string = preg_replace( $pattern, $replacement, $this->_string, $limit ); } else { $this->_string = preg_replace_callback( $pattern, $replacement, $this->_string, $limit ); } return $this; } public function split( $pattern, $limit = -1, $flags = self::WITHOUT_EMPTY ) { return preg_split( static::safePattern($pattern), $this->_string, $limit, $flags ); } public function getIterator() { return new \ArrayIterator(preg_split('#(?_string)); } public function toLowerCase() { $this->_string = mb_strtolower($this->_string); return $this; } public function toUpperCase() { $this->_string = mb_strtoupper($this->_string); return $this; } public function toAscii($try = false) { if (0 === preg_match('#[\x80-\xff]#', $this->_string)) { return $this; } $string = $this->_string; $transId = 'Any-Latin; ' . '[\p{S}] Name; ' . 'Latin-ASCII'; if (null !== $transliterator = static::getTransliterator($transId)) { $this->_string = preg_replace_callback( '#\\\N\{([A-Z ]+)\}#u', function (array $matches) { return '(' . strtolower($matches[1]) . ')'; }, $transliterator->transliterate($string) ); return $this; } if (false === class_exists('Normalizer')) { if (false === $try) { throw new Exception( '%s needs the class Normalizer to work properly, ' . 'or you can force a try by using %1$s(true).', 0, __METHOD__ ); } $string = static::transcode($string, 'UTF-8', 'ASCII//IGNORE//TRANSLIT'); $this->_string = preg_replace('#(?:[\'"`^](\w))#u', '\1', $string); return $this; } $string = \Normalizer::normalize($string, \Normalizer::NFKD); $string = preg_replace('#\p{Mn}+#u', '', $string); $this->_string = static::transcode($string, 'UTF-8', 'ASCII//IGNORE//TRANSLIT'); return $this; } public function transliterate($identifier, $start = 0, $end = null) { if (null === $transliterator = static::getTransliterator($identifier)) { throw new Exception( '%s needs the class Transliterator to work properly.', 1, __METHOD__ ); } $this->_string = $transliterator->transliterate($this->_string, $start, $end); return $this; } public static function getTransliterator($identifier) { if (false === class_exists('Transliterator')) { return null; } return \Transliterator::create($identifier); } public function trim($regex = '\s', $side = 3 ) { $regex = '(?:' . $regex . ')+'; $handle = null; if (0 !== ($side & static::BEGINNING)) { $handle .= '(^' . $regex . ')'; } if (0 !== ($side & static::END)) { if (null !== $handle) { $handle .= '|'; } $handle .= '(' . $regex . '$)'; } $this->_string = preg_replace('#' . $handle . '#u', '', $this->_string); $this->_direction = null; return $this; } protected function computeOffset($offset) { $length = mb_strlen($this->_string); if (0 > $offset) { $offset = -$offset % $length; if (0 !== $offset) { $offset = $length - $offset; } } elseif ($offset >= $length) { $offset %= $length; } return $offset; } public function offsetGet($offset) { return mb_substr($this->_string, $this->computeOffset($offset), 1); } public function offsetSet($offset, $value) { $head = null; $offset = $this->computeOffset($offset); if (0 < $offset) { $head = mb_substr($this->_string, 0, $offset); } $tail = mb_substr($this->_string, $offset + 1); $this->_string = $head . $value . $tail; $this->_direction = null; return $this; } public function offsetUnset($offset) { return $this->offsetSet($offset, null); } public function offsetExists($offset) { return true; } public function reduce($start, $length = null) { $this->_string = mb_substr($this->_string, $start, $length); return $this; } public function count() { return mb_strlen($this->_string); } public function getByteAt($offset) { $length = strlen($this->_string); if (0 > $offset) { $offset = -$offset % $length; if (0 !== $offset) { $offset = $length - $offset; } } elseif ($offset >= $length) { $offset %= $length; } return $this->_string[$offset]; } public function getBytesLength() { return strlen($this->_string); } public function getWidth() { return mb_strwidth($this->_string); } public function getDirection() { if (null === $this->_direction) { if (null === $this->_string) { $this->_direction = static::LTR; } else { $this->_direction = static::getCharDirection( mb_substr($this->_string, 0, 1) ); } } return $this->_direction; } public static function getCharDirection($char) { $c = static::toCode($char); if (!(0x5be <= $c && 0x10b7f >= $c)) { return static::LTR; } if (0x85e >= $c) { if (0x5be === $c || 0x5c0 === $c || 0x5c3 === $c || 0x5c6 === $c || (0x5d0 <= $c && 0x5ea >= $c) || (0x5f0 <= $c && 0x5f4 >= $c) || 0x608 === $c || 0x60b === $c || 0x60d === $c || 0x61b === $c || (0x61e <= $c && 0x64a >= $c) || (0x66d <= $c && 0x66f >= $c) || (0x671 <= $c && 0x6d5 >= $c) || (0x6e5 <= $c && 0x6e6 >= $c) || (0x6ee <= $c && 0x6ef >= $c) || (0x6fa <= $c && 0x70d >= $c) || 0x710 === $c || (0x712 <= $c && 0x72f >= $c) || (0x74d <= $c && 0x7a5 >= $c) || 0x7b1 === $c || (0x7c0 <= $c && 0x7ea >= $c) || (0x7f4 <= $c && 0x7f5 >= $c) || 0x7fa === $c || (0x800 <= $c && 0x815 >= $c) || 0x81a === $c || 0x824 === $c || 0x828 === $c || (0x830 <= $c && 0x83e >= $c) || (0x840 <= $c && 0x858 >= $c) || 0x85e === $c) { return static::RTL; } } elseif (0x200f === $c) { return static::RTL; } elseif (0xfb1d <= $c) { if (0xfb1d === $c || (0xfb1f <= $c && 0xfb28 >= $c) || (0xfb2a <= $c && 0xfb36 >= $c) || (0xfb38 <= $c && 0xfb3c >= $c) || 0xfb3e === $c || (0xfb40 <= $c && 0xfb41 >= $c) || (0xfb43 <= $c && 0xfb44 >= $c) || (0xfb46 <= $c && 0xfbc1 >= $c) || (0xfbd3 <= $c && 0xfd3d >= $c) || (0xfd50 <= $c && 0xfd8f >= $c) || (0xfd92 <= $c && 0xfdc7 >= $c) || (0xfdf0 <= $c && 0xfdfc >= $c) || (0xfe70 <= $c && 0xfe74 >= $c) || (0xfe76 <= $c && 0xfefc >= $c) || (0x10800 <= $c && 0x10805 >= $c) || 0x10808 === $c || (0x1080a <= $c && 0x10835 >= $c) || (0x10837 <= $c && 0x10838 >= $c) || 0x1083c === $c || (0x1083f <= $c && 0x10855 >= $c) || (0x10857 <= $c && 0x1085f >= $c) || (0x10900 <= $c && 0x1091b >= $c) || (0x10920 <= $c && 0x10939 >= $c) || 0x1093f === $c || 0x10a00 === $c || (0x10a10 <= $c && 0x10a13 >= $c) || (0x10a15 <= $c && 0x10a17 >= $c) || (0x10a19 <= $c && 0x10a33 >= $c) || (0x10a40 <= $c && 0x10a47 >= $c) || (0x10a50 <= $c && 0x10a58 >= $c) || (0x10a60 <= $c && 0x10a7f >= $c) || (0x10b00 <= $c && 0x10b35 >= $c) || (0x10b40 <= $c && 0x10b55 >= $c) || (0x10b58 <= $c && 0x10b72 >= $c) || (0x10b78 <= $c && 0x10b7f >= $c)) { return static::RTL; } } return static::LTR; } public static function getCharWidth($char) { $char = (string) $char; $c = static::toCode($char); if (0x0 === $c) { return 0; } if (0x20 > $c || (0x7f <= $c && $c < 0xa0)) { return -1; } if (0xad !== $c && 0 !== preg_match('#^[\p{Mn}\p{Me}\p{Cf}\x{1160}-\x{11ff}\x{200b}]#u', $char)) { return 0; } return 1 + (0x1100 <= $c && (0x115f >= $c || 0x2329 === $c || 0x232a === $c || (0x2e80 <= $c && 0xa4cf >= $c && 0x303f !== $c) || (0xac00 <= $c && 0xd7a3 >= $c) || (0xf900 <= $c && 0xfaff >= $c) || (0xfe10 <= $c && 0xfe19 >= $c) || (0xfe30 <= $c && 0xfe6f >= $c) || (0xff00 <= $c && 0xff60 >= $c) || (0xffe0 <= $c && 0xffe6 >= $c) || (0x20000 <= $c && 0x2fffd >= $c) || (0x30000 <= $c && 0x3fffd >= $c))); } public static function isCharPrintable($char) { return 1 <= static::getCharWidth($char); } public static function fromCode($code) { return mb_convert_encoding( '&#x' . dechex($code) . ';', 'UTF-8', 'HTML-ENTITIES' ); } public static function toCode($char) { $char = (string) $char; $code = ord($char[0]); $bytes = 1; if (!($code & 0x80)) { return $code; } if (($code & 0xe0) === 0xc0) { $bytes = 2; $code = $code & ~0xc0; } elseif (($code & 0xf0) == 0xe0) { $bytes = 3; $code = $code & ~0xe0; } elseif (($code & 0xf8) === 0xf0) { $bytes = 4; $code = $code & ~0xf0; } for ($i = 2; $i <= $bytes; $i++) { $code = ($code << 6) + (ord($char[$i - 1]) & ~0x80); } return $code; } public static function toBinaryCode($char) { $char = (string) $char; $out = null; for ($i = 0, $max = strlen($char); $i < $max; ++$i) { $out .= vsprintf('%08b', ord($char[$i])); } return $out; } public static function transcode($string, $from, $to = 'UTF-8') { if (false === static::checkIconv()) { throw new Exception( '%s needs the iconv extension.', 2, __CLASS__ ); } return iconv($from, $to, $string); } public static function isUtf8($string) { return (bool) preg_match('##u', $string); } public function copy() { return clone $this; } public function __toString() { return $this->_string; } } Consistency::flexEntity('Hoa\Ustring\Ustring'); if (false === Ustring::checkMbString()) { throw new Exception( '%s needs the mbstring extension.', 0, __NAMESPACE__ . '\Ustring' ); } PNG  IHDR{hsRGBgAMA a pHYsod/IDATx^ۏmYY7V jh ҉ 7+!rabl AnL nJ@0!^4!!4ghQ0 &Ч{Ms>_c9kQU֚c̱'yfUWZo9f]wp= {$ H0 a`@= {$ H0 a`@= {$ H0 a`@= {$ H0 a`@= {$ Hǐg P=Tk0}j վǐJSO~:(M9*s@;C*M?4yd{ IqҥU}|2Uv=Tʏ~#A#}Z 怾Ўǐ4J`N怾ЎǐJS:(M9*s@;C*M9*s@;C*M9*s@;C*M?4yd{ I4yd{ 492#1$M92#1$M92#1TJ`N怾Ўǐ4J`N怾ЎǐJSw:(M9*s@;C*M9*s@;C*M9*s@;C*M;4yd{ I4yd{ 4o۪92#1$M92#17׼5]wa=O_Ӹ7W]Ji̩9`:j_c2#1F} 7|K_nSsG:UghGcH|\T}_?xK^rؘU뮻Sos<7]wqd?^zvs9a!7}s2^}ݫF򗿼9K(M96lkmyYooRGVۦjp2#1To,M {|.ЎǐJSɂ߾jz?|>#SOs@>;aʕ+v]v=SߤNkmvcK(M92޿2?Ri*&~ 5=?o|^<seh%=(s@;CoRYxՌ7^M/G[7&zZߵ9gRM~Ӛy%'۽կi4yjNUv=T#<}?&viO|]0|ы^zwm,s@;CoRM9xԶK}_>rIV<sj=ؿRv=TJ.e>ig'?<@V<sj=̽ϥ7tӑwm2#1M~zի^u1zj'&Zs?O}s˥m[9a!nT(Y[o~J`N-9g{ЎǐJSw}9o}nSi̩0}}o>rJuehGcH-5e/{YuZ}=rv=&Zs׃Y?-ЎǐJS+>i|;*r-͑˗/W4yjc^ zVv=ԪoZFK:(M9*s@;C*M9*s@;C*M9*s@;C*MP&}9a!i}&}9a!rJ`N怾Ўǐ4J`N怾Ўǐ4J`N怾ЎǐJST0's@_ehGcH|_0's@_ehGcH{ェ92#1$M92#1T/J`N怾Ўǐ4J`N怾ЎǐJSn馃o*cv]<svUO{nᆃ믿 OxBu?{l'1Zu^wuOyS{]&jmTBSOzғS+>wJ<0?a׾&U6=_y-T`.go?_>8_>tZys}{[zsTf$˿|//~ {]4HvmrPT K]`aY>j/!~?S'7.];VMwwX;s=?=j4y4_~m/_:ܿ/mmvg?>~y>^0aGY{ _{}?| _8x;qWep4|,-oy=g}{WfRYC-uCQa%~ʁ؄׽uJ@ʁl`>0SBj 'JѵX*;r0G `)J[?U*dz]`7˴%dQpŨ?˔ 9,9U>mZj?6 {DJ#dg-D$$eߺ:or[sM-@cۄ=@tY <5 ,S*'6zMd~H]vZ-$)҄Q;rSVvHY༲O)7PI9m]K\٭^lTc.Jއ<=l@uvٯd3g*KǡaN](;MdTvTqTeI.˾tN"#;ҸɎvXk}]7W.[aqdϸ'ӁaA؃JNk_M}n=ԔF]0 x%`Mvٹ%ryNvjUYg T:<@~;Y=d x, 3%_'eeJ@IXˁ̲U>m)ԔW}!1 {0owܔ˒``qK*ɾW~'K'O;Yd`v!'7P)+/F&O]`/#Ks/>oSW~O 0 TN[gWng x {3V)r2g@Sn:r^-X/=Zvr46Sh(vdv:mIJ+RLco%#~q4 g*>:i8VSp'Z-Yf {rv*;;ϳsV`3L٭Ryuv'+/_ x.c#챗NʎN-;ټ ?Tmr6ؾ=;JnWvr`ߔuv9Xt`rjӼ/t{%,`;)A9t T:gI/oNco$XcYCQvH`Ǧ7Pd]w67'<^sµ)^Ȏ C|"kze;7P9jgKc/#ٱ؁쎳lή@%p\ xgqa=g=ټ2xh)}?uv v_Xg= p-aMi2]*ӃN v9uvۓq/nbǰaj+T~[l[tz([g;y-=Z%9{ );FɿQ%TV]Yg'IsNi'Vy,2lrШVuvn\{<|L{ ٤d+AL% $إ?l T# { %Gx$GyA yO97J9^yYg*ʾ3ZKNxr{ #;CrԷ?9j_AƗ@0i첝]{e2aͨrjuv*ٍC$5gzS9p9<]gw\Oj}|h:슰G+; dj7(`ޖ `4?0'a.e'Xfg>24C*KF хڬ]uЕ!6=C& T_9czUyKɁh.XY)sMe@ٙVEF TrU5tq ^eYSztq./7Ps:;9.@@; BأrYUvJ+Ù!wYg~ݴ Tc*<'r``9S 0"af2eGg'Yy+g<Ƨ@%$z玀a&.;ݼ Q@;3Le&7P<k |̥>tU78rI_}^yKyN"OcVd x .s_l.AP&s|#{hz&;h7Jm/eHdgQ^P'12fGm̶ Rڭ 7YgWn1y}ruvƧ7P1s^y嵼\<6#s9_vvRF9k`5mu\Aٳ8a`X@/VuvlK^y><5ؙНawN2\q TMBam'٩3an* IYgL幘a ˀvGc'ʙ ~BBL,G 3y}%g@Tޟ`uɮW;,Kcr X.]Ck*y:A5^e-幚랇تwO9U3ZԆR0<t(d=fHѷQ(7fQn6YgWnbs;.祀aP7y4jK @=!\M__*~ {\X2dp^8tq./7PΎ<1{\H٥b?^\soۦ6JB.8<k$ ,u?HBУre@5J\ӫbxDT y`WmrA8!pCoV ̥rIJseg!q\V(gm+2Ğt uv6J^s^༄=_bJPz TZ=rְ|?m)︿xlGU9ʜAy`&NJ lc^xp'<<vA$jΆՆRXekKxn9E9~s ]8Z%N6ύM*s[.9')v6[`n+RParYY.d]l`70adh=u-מ߄=M^dhgdd]' ~ٮuv2=띟@iք=|O]Yg TvrP`wI}]^^?yxFc*7ήvRX٥ۥˀ2oDޞJ@(LPI7PIuv vX} !g%!SmrveS7_^9XR{=J^Cz0aod)N5!ChM%%dXgׇ<yl{Sjg<F'$2pȥa9 : T<^*P(Dd'o veq.UnBC4{<B2ۑ>a-vÇRXO7}9}9kΒO_c<# ن(2Lv:;O|8OuD3]gw Ty9R#ٕW xeup:ao`>Ax%؝tTYg`g$y_;] x6-G3 u]dh6TDzp^ xppzsE L9 2$..7kئ'yM΢'5L(l7^E]~Mev vJv-<>:.ף! d_"d(<:rJ9/gKZ?7ze$s2I Tr zo ;>?<苰7|9T]>:;"ο#\!לa_xEy;{XuvAqZcyPM^*@? ei7PMPy'-Am-,eΰ xao2|+o*e/z~y=5\%m ~ʠJ6=*ǥKV+z@_@CG*Gi}m{jecnz@_@C217= aO zMhCS;/s*=:?PA}m{jecnz@_@C217= aO zMhCS;/s*=:}{SA}m{jecnz@_@C~217= aO zMhCS;/s*=:};QA}m{jecnz@_@Co۪217= aO zMhCkT3ăOկszI׼RwWK6{ַ~An*u{,s[BX7xc5_??~K6FuZR31%it&鷺/UfO@Co~G>n7m zW^nSz@_K^#q=s5ghCkT {a﮻nSz@;X /nKaFȀ|]b6=A-9k\}=ThCkT {!O؃k|3b>Cm~zֳ hCkTžA{ӛT~9J6{A}h5=?\ݮV~sT z̭pҥk8`~Sn7JІר{uF ~]r]Oec[^-o9O{z6g=س^Za/+ݴ^W>'zz+ېW<ȣ=v/~W?mVn aQMY+}}򓟬~ˠ6$%%M_/}KT= s7VyXn aQ]$쥖4 ئ\™K9yM<=x;qM=@`.C 2LT=կS屨}JS[{Fғ=רa/k^ZnZCY>e屘~Jn{'\{B|`5|͇P]aw]z)/|pu|y4\Ԟ+qE/Z}߷zkuJ6Fu;wtMˠǶ};\W\׽ȟcUZw@^HKM'?<@uʠ6+9rCu?w'>QZ?K7]?m}ݫ-a/qַu {uʠǶRa?uӳ iN~O}SW_|gu֥@^h[ 쬟?gzYJ^.q._\ݮVszVe/{Yu^ʠܖ>9+w>0!׿z(=E+_#C^.ݪmKz@j3|w_uiM/N]z!53zKR=涄ʝuo#^gS/rͶ=m Nj{KS>W~OecnKrZۤ$^h^{okV=涄^wyg~\=y;x_^.ǡki/S5{-s[B8r3?77hCS;/s*=:e-j_=Uz!쩝A}m{jecnz@_@C]:վ zMhCS;/s*=:]rEuP=Uz!쩝A}m{jecnz@_@C˗/ʠJ6=217=ރ>:(s*=ˠJ6=217=<:(s*=ˠJ6{ zMhCS;/s*=ˠJ6{w zMhCS;/s*=:{ェ217= aO zMhc/~QuP=Uz!쩝A}m T_ec.j_zKSAԞ}0!^k_k ն`Nz@얰Iڛw%Q[yڕ7a`{$ H0 a`@= {$ H0 a`@= {$ H0 a`@= {$ H0 a`@= {$ ($IENDB`PNG  IHDRN`sRGBgAMA a pHYsod6NIDATx^۫g}?)-aBK#B/ze(C;QA QBo4z5rXT nDD$XVѨbE$XV ,QPu@X +hTQPu@X +hTQPu@X +t>Ѩ(: Vhb4XѨ(: VhbJhTd+J1UhTd+J17[oum۶_WZF@IQo';w~~ӟfFhb4W^yeE{۶ "s?k53J:VX)&bZokE 'zvQPRu?\=Z +to*k]j.<#i!%E[̫9sfr%L7^ݾ}m~ӧOخThb5_^sUWM{iB4*JWߗ}ۥuTvE:VX)&RZoMTѭDHys3߳X +t1=䠕r-pzkّ~ "jg; 'OnX +DjTɹVp ~hTePGY?X +t1#BG@ʥw'zhTeP7^$]X +DiTIz5\3m(wu׊m}-Mg3Ѩ():fsrX +DhTMh\n?Lh4*Jgյ~RV 3g??5ӧOgK|}vQPRu@ꫧ?/n[2J1z2XBRxe mhTT{PGVShb˜.|YcH.ɱcDz?7Z4*J(eĻvZsz5b@+ SQ77|DjFkE뀒׿䢋.~owYqnZVݨ֓ЍhѨ(:d=+Za`09[`=n6wo>/.R4*J(ۧ\q'OfZa`j_.~7M./ҲKvQPRu@>;nZa`0rG̋/)=rIٳg'FF@I%{nX&Zhbj5lBGnZfFbhTTk裏.Za`Zj==f3{7zҨ(:`+X +t1;ZwY3vk˚V:{QPRuՍ&=gY{u0RLF{汕Asqm1F@I57tӲo4V87+t1]>)ҝ/x<_%z2.w嶭 Jӽw_LOgE:VX)tQPu@X +t1 "QPu@X +hTQPu@X +hTQPu@X +tԩS %YĊu0RF+%YĊu0RLWO<)QPu@X +hTQPu@X +hTQPu@X +tĉ %YĊu0RF+%YĊu0RLW?.QPu@X +hTQPu@X +hTQPu@X +tرc %YĊu0RF+%YĊu0RLW=*QPu@X +hTQPu@X +hTQPu@X +tȑ# %YĊu0RF+%YĊu0RLW>,QPu@X +hTQPu@X +t^Ѩ(: Vhb4XѨ(: VhbX2vܹ3kϚEG\Zt3RLH.:wd۶m;vd=Bҟ-oߞEG\ZtRome,__tJUvۊB9RH&BsϞ=mJJY: Ԯ^&BsKnh TFoo=MD@)uJU{7-&bofo|c!͢QPRm5^zjꭹm"&Y˿gYtʠ9sfzݻ~]vҟ=7|g G=뮛^}ճ_q32I'B7iha02jwT2imU:ڝ%NGZs4v4X{.!\BsС~@<0V@vca`%4GT >4v|_/f`%,XO`\I>?1tVBQ˙hdrW?L4Cc`%G 7}, Wl+*+鷛>UCCd`%,n+Z1Т7{X }};̙3+v}l++yb8c @ 2Vg'WX2+U985ie~&X& PXqw@-,Hq-2RMwSYl"rt_p,6-2R+˲( Sr-3R\O.AjvrObX)ʙ\@mn ǙmZd`E>P[sX)" F.C!kT۳hsV/tTkT{=UXʚVNA%AOL ,ǖήv?&0o-}tOlCTV6xr01':+ oUZ+ä^i_II2oVVpyĽ'MOx ng3<9`8מQFZ Z``eO~e+?S f`eʥ̃r [z[˘'+K z"0[;i-ΰ}) V.芊2)̛}y1\l#`̓KN[u ,BkTHf3 #)n`?{ "Y ֑@Jq@Lnݠ$g,RcJ *s P_:խYVX4,Ȥ FI=w}x}2HjPLM.?Y.jsv2Hנ<<9!HhPDϜ9s,D=:ڔXGߠ<\PVhE5N䂨ҙU^X4tCl:`ѥ}4)0iE"Kh^;r O+\8i .,:@!xy"-޿ٯu4(Z'lWhQTփBX#i+#n-'^e`GSi'lMVuӾ6ցp:Cxr0R/g`!UOf\-q8C8G*OX3,L?ڸT *!c`m3P +Iw rK {ڠU3)+PW]1SX'5K$:Ua`m#KdZr_qpX},Q>cӿE" 0Fk#< ~ǕX9+ G`96`l*4҇V=~ϓ1q;urv1Pg<V_}r `ۂEҐq0XJ62OUhx A@-ϭBe` #ؘ4/h]z}]wmA>H&k0>h9.mAy݉'~K`k҃ϐ_iδJk|AZ=9hM:`հt 8W) 5*/-_% ؚ}a'u<9h+`|Z3AiH`,Z,#@ \X+ߧ8 "s.k@YTsXtd`-}*P#@$޽YZ#;PGjPKu\kA@]iH횔w58xyL[ ߿p *PݻOCL+PuZ@(N\4){Nk+>W?`@L @ĔnJKW=e`]t_'A\%^@+@&,Z+ݰaoS>sb1.37I`6z/D`z"8x mqyL9C|vy:;h2c`݂ҁZY@{rޙArWGXc`݂nqvj0t )}CRa`5@Z:x mKϭ1nAʚ~C` fT|Ow@'繻rz<&u;eоw Vݿ;x 훽bz&utGRSҍ٩AtHtYwДXˀYziAߓX7=ܳTKB;>ǩu`Xr[ou끲 Нy?鲝8鲁tY!ІyMMLΚtq_2[/=~( _K:ڒoڕ>2ٳ&]إzоs<@M=cMÒ>CT?@b=`=Pu|[Z9ge@@=ozF }zb륗^:ܹs}__f@)}˃rb~(? #~~(?Ȋ_}H ;'âĊ@-jAJ 5_Js2,j@ԢĊZ@ij@P #Mqix≳[0j@ԢĊZ@ij@P_|Hv~`QԀXQE-ԀXiXGvNE 5ZԂXQ (M j@?iadXԀXQE-ԀXiXGvNE 5ZԂXQ (M juiadXԀXQE-ԀXiX?3 vNE 5ZԂXQ (M juiadXԀXQE-ԀXiX?S vNE 5ZԂXQ (M juiadXԀXQE-ԀXiXGvNE 5ZԂXQ (M j@O>iadXԀXQE-ԀXiXGvNE 5ZԂXQ (M j@?iadXԀXQE-ԀXiXGvNE 5ZԂXQ (M juiadXԀXQE-ԀXiX?# vNE 5ZԂXQ (M juiadXԀXQE-ԀXiX?C vNE 5ZԂXQ (M juiadXԀXQE-ԀXiX{y'۶mP?ɯ~ϋvNVnl Hя~Qf-(Nvܙ?OS:jլkeQ}>zhX?ݙ֓~nĴs2,-Ԁټ+6\eNĨRiPZ0EV@ 5VYJKCk ;'B g#k /ٟ)jԬ%f~UƤf g}:B (>aSOMwj[oe_m#aikv:e/8BPf-Xt?sK.Yw߽b}-ۦfP (f HYtoP lȏbhX{J#VX)z m2ҜSNe]-oˀ"5ZjւEBxԜyD-Z5DoP X5('z =꺞kfFRX׻;jժ%|uP|`M7:G~xWNxvr-O~&RZ95p:*gJjY '-TO_PmdK?t5]s_7;޽{{RKj5ZjւH}7ͲkJQu``6tis=I|i8MCjV뮻ni\}l(u]5Zjւ(}~vyQ (F ԁj4-윴#]. ޱcDze]6y\ jԬ"ojԀh}>RhXI9ݎ٤{[?.r~E$ )Qk@eRA1Lݾ/}ޚ\}~?ݶTJQ"hu``%t;N:-v~65.fEgak5fb}/_.:5|a5E-5 Jwjsx<ӝ+XzRn\?cCevNꫯ.ݻ{[1?NT@vC@-5kA>K۵kצQ (F ցj4ٝp_:-윴Kv~Jo_5on;R']tҽj>lvQ (V #ׁj4 .;v,]s_zRϧ\Yk@F2{;nW;jԬ|[Z@ij@>PyǦ;SjdV}ݷlgJhXiyG'NnK&픹aiˎnkHwܑ.BjY J"5]V-Ձj4niَ.m%- K 5 e9r$m?~6RCJG `}~v1}XMQ (f HYtoP lqL ;'B HIOݳgϲZ٣)靳/Vl)jԬ󳵣@Z@i5k@"|uP|`M/#"IځyϏvN%=!5V,ogתjլ][-k/[%hiadXZ{rO?YѢPKZ>?{Fc`e,jր̻ϷXZtkƥ~Z95 VjQ bE-45 VZ֑aQbE  VJSb``iZ95 VjQ bE-45 VZtCO ;'âĊ@-jAJ 5:Ҵs2,j@ԢĊZ@ij@P'OiadXԀXQE-ԀXiXGvNE 5ZԂXQ (M juiadXԀXQE-ԀXiXO8!ɰPZ+jB 04- +jP+-Ԁ%@Z95 VjQ bE-45 VZ֑aQbE  VJSb``iZ95 VjQ bE-45 VZcǎIs2,j@ԢĊZ@ij@P #M ;'âĊ@-jAJ 5zQ vNE 5ZԂXQ (M juiadXԀXQE-ԀXiXGvNE 5ZԂXQ (M j@ȑ# - +jP+-HɰPZ+jB (>>|XaQbE  VJSb``iZ95 VjQ bE-45 VZ^{MaQbE  VJSb``iZ95 VjQ bE-45 VZV;'Ò~J퇭fǎ]ve~"ydXrԏ@iٟd۶m??ܹ3MKQ (%ID֜nm(I ZgϞ5&ݮ@IUJRjh?^Vo~sZ d=/TX8 mdrԋ@-?''}ek!jC+9sf{e·~믿>l`<89$C6 0g޽{g[:g[6 j:˚t5}` gζ^veKpnV^}եa;ۺj A{(Ӯ]Y yX* X* X  xX 18X ` ` `l `, ` ` 8X+p1 WCa`  0`^ 0^ 0^ 02^ 0B^ 0b^Df`92+ph ,8@VV  + &+k +8@iV+p lW%X4 x(V+py307^̓ ` ,W[e``,+ExQV `# 8zX+ps1PW1PW9V  @8^$VB  @h^e` Ґ``*kX @U݀KX @UX1 b`%+Uug}&b`%+UXcJ$VO?TJ$V2ƊH Te`+X> +X+V"1PU7 } V"1P5V Db`*kX @U݀GIX @UX1?1 b`%+UEXoɶm۲я~駟Ϝg Db`n@W^ܹ3;P}/srYϾ{_*V"1PUu3k /ٟep͡J$VJ}ɵ^bhꪫ&o֊_~gZO>b۔Gms̙%\6b`%+UX۷lP\mP߿nvO=t? /dlWyH T H^̞-Mo6КC-wygzw/sugYs-*V"1PU5oW\qԩSJ_ڵkg{衇-*V"1PUu^g2gy֛ٟg`e T H͒9}3imwK.hs|vM?nm4V"1PU5Y\09zhv2{iɓ'cm۷O~_f[T Db`n@J–΋/h8O86G}t)wqDz_liso3X @U5֔ni: 3GnOLYms \"V"1PU5=wϞ=!jP./~bmx1)=lV}u=I3<]f/>WYÇg֢c`%+UEX{r\~Ǐgl;ng."V"1PU7 Ks$]{gʔ[O|XJ$V2ƊH T HH Te`+X$V D``ܰ$c`%+vm+& @8x s +X @HVB2 d` $+!X @HVB2 d` $+!X @HVB2 d` $+!X @HVB2 d` $+M&X&Nk6IENDB`PNG  IHDR^:DsRGBgAMA a pHYsod*$IDATx^ɯee`~#b 82&BL8˜8@84!N1LҌT&0F#*^>{jWm~ȭ}Z9i *3|P Tg:'> @uO3|P Tg:'> @uO3|P Tg:'> @uO3|P Tg:'U}_z>jI|$(OܿoIMr:hOg}& Вu@XIUNt/rmjYu TO?T}F`+D3|R+-Yu TO>DD%\ ᓪ4\th: Wf*M'W4Z>+r$@Kb@4'Ui:Вu@XIU]裏$A4Z>JMr:hOtrE%\ ᓪ"Jh:d+D3|R+-Yu T>@D%\ ᓪ4\th: Wf*M'W4Z>+r׿$A4Z>JMr:hO?OIMr:hOtrE%\ ᓪtꪵN:i=r_MVߟ}ٵ38c5;?'"D3|RUWg9\ێ%-e\}c C_efMgpT8^{M2V|Ή=ZIU]{Fwyg{z( G?Ѣq~탟3h:m~ᵳ:k%]wQ]y)СCGm*D3|RU<Ӌh;U{8.C4ZʶXUr_JxŶ%CCjXIU]{wG~S)M߬Jy?/{4ZʶXEgO3]?3e}7>*[Lzť4]#NM2V߷3Q.}W>*Sl t4NRu{ ϩ>+rg?٢ WZ|7pyc@K[|2|*KLʃ.õ^{6>itN4Zʰ{hm>*ClG&%9>v?O4F 뀨+rғ̹{64SO]{nh:h˥gy憯s- n>*l6W\qņFdj,Nj@K뀖Z;];mTf観 j.--EZ3Y ᓪ"WȚ{Cx饗[΍7޸]v.S4Z\Ko+r͛3'`nR. [t8p`p,th)jТ du Tt6~(⋃+ˍg+-"-Ejkßa, ᓪf|ds;cjR:vO]fȕ7nΖo펗Cmh@ /0mh:ߗzn5!F?Q߳Efp Fq mɵ@K/ɜ>+rL)|K_Z4m&_sYomRu@^=NC ~ڱ ᓪZ79~4Z>+r @Kb@4'Ui:Вu@XIUNh:d+D3|RUW^}UIMr:hOtrE%\ ᓪ"wAIMr:hOtrE%\ ᓪ4\th: Wf8p@D%\ ᓪ4\th: WfȽ+ -Yu T䊦@Kb@4'Ui:Вu@XIU]{%A4Z>JMr:hOK/$ Вu@XIUNh:d+D3|R+-Yu T_|QD%\ ᓪ4\th: WfȽ -Yu T䊦@Kb@4'UuE矗th: Wf*M'W4Z>+r-sgq)g,?ПՎ@ C=vN>j'vI'޽{3lgܵk׎@ C=vJo:Yvb|^/%oɐ_ .ܦeSZz6R(Y.C۴ bZM?񢐗&S2Њuc>in+ϕW^(_ɘ~?MzNSzh饥:MƔ5K~O~2MXВ;|>={x}?{;?9Q`3J,=ul.:;Q&'VNyR\)cަw(]-ufdʙha9:VHgwNl*g ٭ Sedݻ^Kp?\s5G> 馛};v' P\@4Sed4pw_ NyPtk9]'1ÜeES{\3OFax|D35Oқ`=x9`S|;ǿ?b$5}_<KM~3Ws?t>I˥&u}x.sǍv+d$% hp W_&ᓔdr69?܂98>IǑ\L;2pv|,d$7o'0EN4G:dr)Yw0|#xΕPpx)p5cL yq1󼇝b, rn54nƨy-_Ef%+'aܧZiw >rgϞ=G>v10|{xL2cPu:S:sr& 4\=9c?XZ΀:n!3'Maȓm*p d6ĕPdb29+N;;L&Oq?b[<80ږ[qIKl<ٶ(nNIur vg21|Re7DrM<'I5eqG彼m7EOp-rk:{=QJwJ'Кᓕrfm<Z=`3D1|2 x8* @ =5hJ<=v<UrњsmQ8 v'DҒۡhD'WҌSj[y5>vLlV l3f?^NgAЂm) <1 z[4w޹ l#cUNt8;s1!pe5>gǦ~p<TN_ZYmVɖlih'CAݻwrr?/5>4OMei_ͪ>gp8(9?tjON4ndyt/|9=_eX8߽{|)'/8l;OB{٠oRǮLМH9 Q|}Ľlᓅ~!TSǾ0M;Ux+>6:PUǮ{.l)8KV>Yqp;~-8'`ʎO-g'?ls\.*8Z0^baJwa={lN&>!?DY 3e_*( 1|T$+f*اEfSsgb\ 0.Ox 35QCk:zSS>b3|Όт 1hj`sF<͎V\#hv>gVLi6e\xS(g?6Yfl!hɾC={|{>'4O%Kb9D4g>'k6lK$O/d`4s42>QjrL'-XΓsƓme_m6LY>><>'e dUxz@}eQ[|#91 ٕ}%:ze1adVn9!^Ȍtoad<>'400&:z6ȘF۷G2Dh6Q 3*a9} u{([ u(a9G9;Oؙ~uc.e17j3e_.q'l+H <>G ӓmgνL+g!G*Oؼ3L9B S߿Xj=]ϑqfsf\XNsDb.,r]}=cO׿3޳h aN\ 5=>̕S) \\39G|;-}’;Ga܎3\kG:ׁ0oC(C(̑𙘧|+g|m q2|&ld̍op4㌛3BOO\`Nrú2wg2;l G_`[o݁ 5⪀q1|&R^8.s 0en런q;x>qy lKn)r lq395e_r0v7w l+ lKnp l_wI \.;S:Խ:0V+P`k^C>9kQn\`lo_J[q0|r40F7z;ӿo<ȓ| A<V;  V*7ggh2pz.0ay>bP{s2|6ԿÓmP Zو1Ж#@F+<*W gxxooh:<g@_? J- \m1geμ@2pv {~x᳢}-vru@GOԂZ.g?<܇bݎɶVJ7xzu/6 + !P\0e^^%nk"{<sIP0.nvZ']`cܢ|wmx0>Coн',U gܢnZvP4tͥr\X끲0u ƭ׶X=uor뭷zLr)0X`'빻z`u []bSvHHa7Y0~W2Xs Kl#%lJ>(rVyw ʒ/| u [tu,NKp+_X!ttן)t;r{qi,u}ĥ62UW]Xj>7;#}n7eG-˥{8ki0cIw6KmN za=s@;gܤK.dإ)G;U^R]R߉e`g |$Maz{H(`0|nB}kׅV^w}7 :ַe="U/˓gk׮/~>@+CR^_.1Z%>'`aO hehDh2|ј'5 WК+5gIDh̓+jQԂ\Q hM ȕ`Q{`Ԁ\QZ@kj@Dր&租~* >Ԣ@ WZSr%>gyRrE Z+jY |$H<E-Ԁ\%rGcԀ\QZ@kj@D1Oj@DQ rE-55 W"k@?ј'5 WК+59Dh̓+jQԂ\Q hM ȕd裏$A"w4I 5(jAJd 0|(;@ WZSr%>gyRrE Z+jY ~$H<E-Ԁ\%rGcԀ\QZ@kj@Dր&| 1Oj@DQ rE-55 W"ksFј'5 WК+59Dh̓+jQԂ\Q hM ȕd׿% 1Oj@DQ rE-55 W"ksFј'5 WК+5S$rGcԀ\QZ@kj@D1Oj@DQ rE-55 W"kχzhNRN9唵_/s"w4i 5xꪫk@ɯ5(ug8{w ~NYZ}>{M?m|c3͘yC X3FYSϏDRɘyC xG@N;m^%s]x2O5}~Lu 4>{エy;y睷on ;N!:4mDhj@;S}oX5駟>@ZP>|xڰu]wvW^ym"kZ@k5v[m4o.S"w4i 5dlwze n%jQ"kA>߯,_J?KEZ%@d h2|is/~eG|7JG.;61Oc ̭ցn!jQ"kA>oo8R,5*jEր}~u >wД;,7r˫:[om&s-PY jv?vj*ZT hX"ky, ʡkvFD. 9Z@kQ5EcMrow}s]{~vmdJٔl8b<iٳgھ}Ȱ5tQY ~ݹKUɾ@d h2|뭳f)ڮ))GC˖(2t'wG\=wRRDւL}Kj}=g5kJ[M9}裏~݌јXJ)z袋Q_P?@knJQY fQ X끌}>K41-X#w4\bS.ٽ{gvM7o>ډ3Z*e]d@d h2|##YS`)+_?Sb҄JրrrSnJjrDa޺}oh>_ /nm[.?HD2lug)S߇ܮy+"k׮5ٶ0oݾ7_Nd8xEg@>TIzYN3g=ܳovCoaG[˒izʽc6?F\o\rvS@Z}\gy?fV-z CZ"ksMiyNƴyrt~Ik5+wQY "SO=v/># nUɾ@d 0|)wSO=u_.C"w4<_rpD|msZG-`ư@d h2|k^)7X7X/v1-RˏR_g5J3Dy'7ݼ@ZвkUɾc5æ|:?c1Ocg5si,u@ Jd-h_|vZd hR"k@᳜ΚktR.M);vY1OcO<Ć[R_bp QY ZvYZd hT"ksM/߰SCeI<,ˑ_|qp~.J i5(v_^f?:Y M^fHvRvƇ~xgJ<t)%jLY j~ڡZd RϏDwr^1Oc˹_9眳+ ~lQY jK'sY>?:Y tt֔7N#Y1OcJdK_?e@ZPϗKs?lʢ5Zd 8VVZ"k9%rGcԀ\QZ@kj@Dր&gZ1Oj@DQ rE-55 W"ksFј'5 WК+59Dh̓+jQԂ\Q hM ȕd,7J|"w4I 5(jAJd 0|(;@ WZSr%4><( 1Oj@DQ rE-55 W"ksFј'5 WК+59Dh̓+jQԂ\Q hM ȕd_z%IyRrE Z+jY 3J<E-Ԁ\%rGcԀ\QZ@kj@Dր&/( 1Oj@DQ rE-55 W"ksFј'5 WК+5 /HDh̓+jQԂ\Q hM ȕ`Q"w4I 5(jAJd h2|> ;@ WZSr%>gyRrE Z+jY +;4I|Z%>j `a"v4ih6J|Z$>59ꫯ>/*Ԁ|jA@Kj@`#hC  K ͇KѠ3J\ ֆCdO @uO3|P Tg:'> @uO3|P Tg:'> @uO3|P Tg:'> @uO3|P Tg:'> @uO*[[{-IENDB`PNG  IHDR]ݽsRGBgAMA a pHYsodIDATx^ݹoTqҦB2NED("҄%R2U@$+$& Q5+P@J?\YHk̽g&S*$$$$$$$$$$$$$$$$$$$$$$$$$$$$$sPJ(@H իWA(@(!) ܗ/_$ W/ÇG~[O $ aIPPDBRϟ?'BBIPPDB+ QgψPPDB+ QӧOPPDB+ QD E $  QD E $c  QD E $QBBIa>z8E $QBBIa>|8E $QBBIaÆʔ)S6mZԩSTBBIa>x@.Νtuuehپ}{XPPDBrO5g1cFBBIa޿_*wޭ,[,+/2+G?G! QZgϞ..ZrW^PPDBRݓʮ]۶m[M!f'OF?{(@(!Io߾]3gNMݹs'$J1CBIxѬ͛7?+\) Qm*Qɺu벢oߺu'j>O! QVo+|9ZBBIa=49r$+ V_7ڟ#._\CBIJh_.]֭[8g}qPPDBR?5/u8~xkf{9 Q5vڵqV8+cǎE (@HR)@dɒg8ˍ7j.uPPDBRvs:D3}ŋ_[(@(!I׬Y-f޽ѯ- QPeboV\BBIa=6rPܹ3_=S(@(!I?WX=&3gTfΜ}-[y E $k/ ӧOה؞={bN/^} ;K(@(!{/cq%h$X .Ǝ+ '5 (@H ^<-wMyƊ[LDY@X$cǎׁcL+ Q]n۬2k֬>qIך?~ab (@HZ" {ePPDB+ QD E ${U  QD E ${  QD E $QBBIaڻCBIPPDBR.]"BBIPPDB+ QH$ W(@(!) ܡ!  QD E $QBBIa$ W(@(!) ܁  QD E ${  QD E $̞=3OѾ؟: PԩS+SLtvvFC{ﱣ#% Jcƍo fO>ᅬ!7oξŋGig(@F 𫯾J=vx~(@F___ӳ~L>1Vw}3Ďiu(@xGuww|TǼy߻ _("0l'S@3D/<{gΟ?=]|G S- 'Of?ϦMF> rvޝ{YA#^( {X6v ~6$Zg@(@w|WjæDR-i?[x4YQH^jcg( IKX|H d3Ҋ/V@j(@$HpVPDRR9^ E(@$CBϊ" ~PH9> EJ(@^ 72RAԸ59lB (@'8 bx}eGlBQ(v26m r@YP(^'΃!g"3ᑏ:(@H^T u {~PBB :g|v|X(!͞Nah`g(Pp:G % \cǧvBx6+3Qp+?Mzo (@Ďre+; B |h= m@İ.P mÎO%\ڂD[. x+=+?[#V@Qh9vaX+h' -ŎOLmbag(ZDKPagAQh v|Vt{8D#;Cr:L ]|h6ZD:.4  5h%JXh`ځuf0^v+ DCx^P4Ĥ;Ea(@L ;>Q4vQ &h c'<ɯᑏoGbB ɢ1nsW/l(06 ®;xgk4<@(0: o뮠 c سPlX(@Pdúeg(FCbT rŽcg(ކ`g(B;>Q6\h(@dobg(b(@d;( *!z Gd y`%kZÚ׺"]`xRU 0QAx?PJ8vLDQ.;>7寎|eF&34-`_xRr`+=+pE;>agh(bWΡޑL(+V|Z>`ɰh.X2DT`gh9Q%O^>` whM}8e2C ˍ} o/ P;Ӏb?'& P@bY#܃F _x . P6O7Ez(@<43T \;>{zzF> (@]h@:ad.{J0ܺ;(D8Y츏C(<;\t"_~a'P;Cţ?Q";>rg~኎=# ]< Ёp3v^˚绝(X> ;?2hq$ pܹHWWWD\-@֟Z켷.\1j(@Ҵ[.W^Q]  ]/_$+3$ '>>|x43WTgtx8H}؂hf 4]_Qs :(@pT?t1|EuP.~b .gϞQ] @+3@>}JDuC3WTgH.f 4]_Q :(@pT?t1|EuHǏ.~b IQ] @+3@=zDDuC3WTgH.f|!q]_Q i8:(L2eB6mZԩSѯ9fXٰaCtXx .o]I(] 3>ΝtuuEX@(EU?t)̀|z7Vf̘Q~zkz .ͯ-EUnݺ=.]v]=ST?t)ݻw+˖-9-̈́gϾ,Qa ELZOG] 3~\,0zq^: ޽{nsњEqO[crqmp kbæzg@]ǎ?ܲeK: Uk<-Z+@{A{Ѳsy?v^ _zgݷ_ű9s2sٳ'z\Q`h_B3f@UZϟ_}+ ]vykEآžר.~R.}Κ5+z[y IQ]  ]vm]_Q i8:(@pT?t1|EuHn]_Q i8: ]_Q i8:(@pT?t1|EuH)>:(@pT?t1|EuHQ,)>:(@pT?t1|EuP.~b .@⣺+3$ GuC3WTgt Q] @+3$ GuC3WTgtQ] @+3@.~b IQ]  ].\ +3$ GuC3WTgt_Q[[ 4-jb Y17n|/HZ]i 8G!). NQ$Q$Q$Q$Q$Q$Q$Q$Q$Q$Q$Q$Q$Q$Q$Q$Q$Q$Q$Q$Q$Q$Q$Q$Q$Q$Q$Q$Q$Q$Q$Q$QU*uWIENDB`aProp; } public function setAProp($prop) { $this->aProp = $prop; return $this; } } bProp; } public function setBProp($prop) { $this->bProp = $prop; return $this; } } prop1; } public function setProp1($prop) { $this->prop1 = $prop; return $this; } public function getProp2() { return $this->prop2; } public function setProp2($prop) { $this->prop2 = $prop; return $this; } } name = $name; } public function getProp() { return $this->prop; } public function setProp($prop) { $this->prop = $prop; return $this; } }cloned = true; } } aProp; } public function setAProp($prop) { $this->aProp = $prop; return $this; } public function __clone() { $this->cloned = true; } } bProp; } public function setBProp($prop) { $this->bProp = $prop; return $this; } public function __clone() { $this->cloned = true; } } cloned = true; } } cloned = true; } } foo = $foo; } public function getFoo() { return $this->foo; } } useCloneMethod = $useCloneMethod; $this->addTypeFilter(new DateIntervalFilter(), new TypeMatcher(DateInterval::class)); $this->addTypeFilter(new SplDoublyLinkedListFilter($this), new TypeMatcher(SplDoublyLinkedList::class)); } public function skipUncloneable($skipUncloneable = true) { $this->skipUncloneable = $skipUncloneable; return $this; } public function copy($object) { $this->hashMap = []; return $this->recursiveCopy($object); } public function addFilter(Filter $filter, Matcher $matcher) { $this->filters[] = [ 'matcher' => $matcher, 'filter' => $filter, ]; } public function addTypeFilter(TypeFilter $filter, TypeMatcher $matcher) { $this->typeFilters[] = [ 'matcher' => $matcher, 'filter' => $filter, ]; } private function recursiveCopy($var) { if ($filter = $this->getFirstMatchedTypeFilter($this->typeFilters, $var)) { return $filter->apply($var); } if (is_resource($var)) { return $var; } if (is_array($var)) { return $this->copyArray($var); } if (! is_object($var)) { return $var; } return $this->copyObject($var); } private function copyArray(array $array) { foreach ($array as $key => $value) { $array[$key] = $this->recursiveCopy($value); } return $array; } private function copyObject($object) { $objectHash = spl_object_hash($object); if (isset($this->hashMap[$objectHash])) { return $this->hashMap[$objectHash]; } $reflectedObject = new ReflectionObject($object); $isCloneable = $reflectedObject->isCloneable(); if (false === $isCloneable) { if ($this->skipUncloneable) { $this->hashMap[$objectHash] = $object; return $object; } throw new CloneException( sprintf( 'The class "%s" is not cloneable.', $reflectedObject->getName() ) ); } $newObject = clone $object; $this->hashMap[$objectHash] = $newObject; if ($this->useCloneMethod && $reflectedObject->hasMethod('__clone')) { return $newObject; } if ($newObject instanceof DateTimeInterface || $newObject instanceof DateTimeZone) { return $newObject; } foreach (ReflectionHelper::getProperties($reflectedObject) as $property) { $this->copyObjectProperty($newObject, $property); } return $newObject; } private function copyObjectProperty($object, ReflectionProperty $property) { if ($property->isStatic()) { return; } foreach ($this->filters as $item) { $matcher = $item['matcher']; $filter = $item['filter']; if ($matcher->matches($object, $property->getName())) { $filter->apply( $object, $property->getName(), function ($object) { return $this->recursiveCopy($object); } ); return; } } $property->setAccessible(true); $propertyValue = $property->getValue($object); $property->setValue($object, $this->recursiveCopy($propertyValue)); } private function getFirstMatchedTypeFilter(array $filterRecords, $var) { $matched = $this->first( $filterRecords, function (array $record) use ($var) { $matcher = $record['matcher']; return $matcher->matches($var); } ); return isset($matched) ? $matched['filter'] : null; } private function first(array $elements, callable $predicate) { foreach ($elements as $element) { if (call_user_func($predicate, $element)) { return $element; } } return null; } } copy($value); } setAccessible(true); $oldCollection = $reflectionProperty->getValue($object); $newCollection = $oldCollection->map( function ($item) use ($objectCopier) { return $objectCopier($item); } ); $reflectionProperty->setValue($object, $newCollection); } } setAccessible(true); $reflectionProperty->setValue($object, new ArrayCollection()); } } __load(); } } callback = $callable; } public function apply($object, $property, $objectCopier) { $reflectionProperty = ReflectionHelper::getProperty($object, $property); $reflectionProperty->setAccessible(true); $value = call_user_func($this->callback, $reflectionProperty->getValue($object)); $reflectionProperty->setValue($object, $value); } } setAccessible(true); $reflectionProperty->setValue($object, null); } } class = $class; $this->property = $property; } public function matches($object, $property) { return ($object instanceof $this->class) && $property == $this->property; } } property = $property; } public function matches($object, $property) { return $property == $this->property; } } propertyType = $propertyType; } public function matches($object, $property) { try { $reflectionProperty = ReflectionHelper::getProperty($object, $property); } catch (ReflectionException $exception) { return false; } $reflectionProperty->setAccessible(true); return $reflectionProperty->getValue($object) instanceof $this->propertyType; } } getProperties(); $propsArr = array(); foreach ($props as $prop) { $propertyName = $prop->getName(); $propsArr[$propertyName] = $prop; } if ($parentClass = $ref->getParentClass()) { $parentPropsArr = self::getProperties($parentClass); foreach ($propsArr as $key => $property) { $parentPropsArr[$key] = $property; } return $parentPropsArr; } return $propsArr; } public static function getProperty($object, $name) { $reflection = is_object($object) ? new ReflectionObject($object) : new ReflectionClass($object); if ($reflection->hasProperty($name)) { return $reflection->getProperty($name); } if ($parentClass = $reflection->getParentClass()) { return self::getProperty($parentClass->getName(), $name); } throw new PropertyException( sprintf( 'The class "%s" doesn\'t have a property with the given name: "%s".', is_object($object) ? get_class($object) : $object, $name ) ); } } $propertyValue) { $copy->{$propertyName} = $propertyValue; } return $copy; } } callback = $callable; } public function apply($element) { return call_user_func($this->callback, $element); } } copier = $copier; } public function apply($element) { $newElement = clone $element; $copy = $this->createCopyClosure(); return $copy($newElement); } private function createCopyClosure() { $copier = $this->copier; $copy = function (SplDoublyLinkedList $list) use ($copier) { for ($i = 1; $i <= $list->count(); $i++) { $copy = $copier->recursiveCopy($list->shift()); $list->push($copy); } return $list; }; return Closure::bind($copy, null, DeepCopy::class); } } type = $type; } public function matches($element) { return is_object($element) ? is_a($element, $this->type) : gettype($element) === $this->type; } } fqsen = $fqsen; if (isset($matches[2])) { $this->name = $matches[2]; } else { $matches = explode('\\', $fqsen); $this->name = trim(end($matches), '()'); } } public function __toString() { return $this->fqsen; } public function getName() { return $this->name; } } lineNumber = $lineNumber; $this->columnNumber = $columnNumber; } public function getLineNumber() { return $this->lineNumber; } public function getColumnNumber() { return $this->columnNumber; } } bodyTemplate = $bodyTemplate; $this->tags = $tags; } public function getTags() { return $this->tags; } public function render(Formatter $formatter = null) { if ($formatter === null) { $formatter = new PassthroughFormatter(); } $tags = []; foreach ($this->tags as $tag) { $tags[] = '{' . $formatter->format($tag) . '}'; } return vsprintf($this->bodyTemplate, $tags); } public function __toString() { return $this->render(); } } tagFactory = $tagFactory; } public function create($contents, TypeContext $context = null) { list($text, $tags) = $this->parse($this->lex($contents), $context); return new Description($text, $tags); } private function lex($contents) { $contents = $this->removeSuperfluousStartingWhitespace($contents); if (strpos($contents, '{@') === false) { return [$contents]; } return preg_split( '/\{ # "{@}" is not a valid inline tag. This ensures that we do not treat it as one, but treat it literally. (?!@\}) # We want to capture the whole tag line, but without the inline tag delimiters. (\@ # Match everything up to the next delimiter. [^{}]* # Nested inline tag content should not be captured, or it will appear in the result separately. (?: # Match nested inline tags. (?: # Because we did not catch the tag delimiters earlier, we must be explicit with them here. # Notice that this also matches "{}", as a way to later introduce it as an escape sequence. \{(?1)?\} | # Make sure we match hanging "{". \{ ) # Match content after the nested inline tag. [^{}]* )* # If there are more inline tags, match them as well. We use "*" since there may not be any # nested inline tags. ) \}/Sux', $contents, null, PREG_SPLIT_DELIM_CAPTURE ); } private function parse($tokens, TypeContext $context) { $count = count($tokens); $tagCount = 0; $tags = []; for ($i = 1; $i < $count; $i += 2) { $tags[] = $this->tagFactory->create($tokens[$i], $context); $tokens[$i] = '%' . ++$tagCount . '$s'; } for ($i = 0; $i < $count; $i += 2) { $tokens[$i] = str_replace(['{@}', '{}', '%'], ['@', '}', '%%'], $tokens[$i]); } return [implode('', $tokens), $tags]; } private function removeSuperfluousStartingWhitespace($contents) { $lines = explode("\n", $contents); if (count($lines) <= 1) { return $contents; } $startingSpaceCount = 9999999; for ($i = 1; $i < count($lines); $i++) { if (strlen(trim($lines[$i])) === 0) { continue; } $startingSpaceCount = min($startingSpaceCount, strlen($lines[$i]) - strlen(ltrim($lines[$i]))); } if ($startingSpaceCount > 0) { for ($i = 1; $i < count($lines); $i++) { $lines[$i] = substr($lines[$i], $startingSpaceCount); } } return implode("\n", $lines); } } getFilePath(); $file = $this->getExampleFileContents($filename); if (!$file) { return "** File not found : {$filename} **"; } return implode('', array_slice($file, $example->getStartingLine() - 1, $example->getLineCount())); } public function setSourceDirectory($directory = '') { $this->sourceDirectory = $directory; } public function getSourceDirectory() { return $this->sourceDirectory; } public function setExampleDirectories(array $directories) { $this->exampleDirectories = $directories; } public function getExampleDirectories() { return $this->exampleDirectories; } private function getExampleFileContents($filename) { $normalizedPath = null; foreach ($this->exampleDirectories as $directory) { $exampleFileFromConfig = $this->constructExamplePath($directory, $filename); if (is_readable($exampleFileFromConfig)) { $normalizedPath = $exampleFileFromConfig; break; } } if (!$normalizedPath) { if (is_readable($this->getExamplePathFromSource($filename))) { $normalizedPath = $this->getExamplePathFromSource($filename); } elseif (is_readable($this->getExamplePathFromExampleDirectory($filename))) { $normalizedPath = $this->getExamplePathFromExampleDirectory($filename); } elseif (is_readable($filename)) { $normalizedPath = $filename; } } return $normalizedPath && is_readable($normalizedPath) ? file($normalizedPath) : null; } private function getExamplePathFromExampleDirectory($file) { return getcwd() . DIRECTORY_SEPARATOR . 'examples' . DIRECTORY_SEPARATOR . $file; } private function constructExamplePath($directory, $file) { return rtrim($directory, '\\/') . DIRECTORY_SEPARATOR . $file; } private function getExamplePathFromSource($file) { return sprintf( '%s%s%s', trim($this->getSourceDirectory(), '\\/'), DIRECTORY_SEPARATOR, trim($file, '"') ); } } indent = $indent; $this->indentString = $indentString; $this->isFirstLineIndented = $indentFirstLine; $this->lineLength = $lineLength; $this->tagFormatter = $tagFormatter ?: new DocBlock\Tags\Formatter\PassthroughFormatter(); } public function getDocComment(DocBlock $docblock) { $indent = str_repeat($this->indentString, $this->indent); $firstIndent = $this->isFirstLineIndented ? $indent : ''; $wrapLength = $this->lineLength ? $this->lineLength - strlen($indent) - 3 : null; $text = $this->removeTrailingSpaces( $indent, $this->addAsterisksForEachLine( $indent, $this->getSummaryAndDescriptionTextBlock($docblock, $wrapLength) ) ); $comment = "{$firstIndent}/**\n{$indent} * {$text}\n{$indent} *\n"; $comment = $this->addTagBlock($docblock, $wrapLength, $indent, $comment); $comment .= $indent . ' */'; return $comment; } private function removeTrailingSpaces($indent, $text) { return str_replace("\n{$indent} * \n", "\n{$indent} *\n", $text); } private function addAsterisksForEachLine($indent, $text) { return str_replace("\n", "\n{$indent} * ", $text); } private function getSummaryAndDescriptionTextBlock(DocBlock $docblock, $wrapLength) { $text = $docblock->getSummary() . ((string)$docblock->getDescription() ? "\n\n" . $docblock->getDescription() : ''); if ($wrapLength !== null) { $text = wordwrap($text, $wrapLength); return $text; } return $text; } private function addTagBlock(DocBlock $docblock, $wrapLength, $indent, $comment) { foreach ($docblock->getTags() as $tag) { $tagText = $this->tagFormatter->format($tag); if ($wrapLength !== null) { $tagText = wordwrap($tagText, $wrapLength); } $tagText = str_replace("\n", "\n{$indent} * ", $tagText); $comment .= "{$indent} * {$tagText}\n"; } return $comment; } } '\phpDocumentor\Reflection\DocBlock\Tags\Author', 'covers' => '\phpDocumentor\Reflection\DocBlock\Tags\Covers', 'deprecated' => '\phpDocumentor\Reflection\DocBlock\Tags\Deprecated', 'link' => '\phpDocumentor\Reflection\DocBlock\Tags\Link', 'method' => '\phpDocumentor\Reflection\DocBlock\Tags\Method', 'param' => '\phpDocumentor\Reflection\DocBlock\Tags\Param', 'property-read' => '\phpDocumentor\Reflection\DocBlock\Tags\PropertyRead', 'property' => '\phpDocumentor\Reflection\DocBlock\Tags\Property', 'property-write' => '\phpDocumentor\Reflection\DocBlock\Tags\PropertyWrite', 'return' => '\phpDocumentor\Reflection\DocBlock\Tags\Return_', 'see' => '\phpDocumentor\Reflection\DocBlock\Tags\See', 'since' => '\phpDocumentor\Reflection\DocBlock\Tags\Since', 'source' => '\phpDocumentor\Reflection\DocBlock\Tags\Source', 'throw' => '\phpDocumentor\Reflection\DocBlock\Tags\Throws', 'throws' => '\phpDocumentor\Reflection\DocBlock\Tags\Throws', 'uses' => '\phpDocumentor\Reflection\DocBlock\Tags\Uses', 'var' => '\phpDocumentor\Reflection\DocBlock\Tags\Var_', 'version' => '\phpDocumentor\Reflection\DocBlock\Tags\Version' ]; private $tagHandlerParameterCache = []; private $fqsenResolver; private $serviceLocator = []; public function __construct(FqsenResolver $fqsenResolver, array $tagHandlers = null) { $this->fqsenResolver = $fqsenResolver; if ($tagHandlers !== null) { $this->tagHandlerMappings = $tagHandlers; } $this->addService($fqsenResolver, FqsenResolver::class); } public function create($tagLine, TypeContext $context = null) { if (! $context) { $context = new TypeContext(''); } list($tagName, $tagBody) = $this->extractTagParts($tagLine); if ($tagBody !== '' && $tagBody[0] === '[') { throw new \InvalidArgumentException( 'The tag "' . $tagLine . '" does not seem to be wellformed, please check it for errors' ); } return $this->createTag($tagBody, $tagName, $context); } public function addParameter($name, $value) { $this->serviceLocator[$name] = $value; } public function addService($service, $alias = null) { $this->serviceLocator[$alias ?: get_class($service)] = $service; } public function registerTagHandler($tagName, $handler) { Assert::stringNotEmpty($tagName); Assert::stringNotEmpty($handler); Assert::classExists($handler); Assert::implementsInterface($handler, StaticMethod::class); if (strpos($tagName, '\\') && $tagName[0] !== '\\') { throw new \InvalidArgumentException( 'A namespaced tag must have a leading backslash as it must be fully qualified' ); } $this->tagHandlerMappings[$tagName] = $handler; } private function extractTagParts($tagLine) { $matches = array(); if (! preg_match('/^@(' . self::REGEX_TAGNAME . ')(?:\s*([^\s].*)|$)/us', $tagLine, $matches)) { throw new \InvalidArgumentException( 'The tag "' . $tagLine . '" does not seem to be wellformed, please check it for errors' ); } if (count($matches) < 3) { $matches[] = ''; } return array_slice($matches, 1); } private function createTag($body, $name, TypeContext $context) { $handlerClassName = $this->findHandlerClassName($name, $context); $arguments = $this->getArgumentsForParametersFromWiring( $this->fetchParametersForHandlerFactoryMethod($handlerClassName), $this->getServiceLocatorWithDynamicParameters($context, $name, $body) ) ; return call_user_func_array([$handlerClassName, 'create'], $arguments); } private function findHandlerClassName($tagName, TypeContext $context) { $handlerClassName = Generic::class; if (isset($this->tagHandlerMappings[$tagName])) { $handlerClassName = $this->tagHandlerMappings[$tagName]; } elseif ($this->isAnnotation($tagName)) { } return $handlerClassName; } private function getArgumentsForParametersFromWiring($parameters, $locator) { $arguments = []; foreach ($parameters as $index => $parameter) { $typeHint = $parameter->getClass() ? $parameter->getClass()->getName() : null; if (isset($locator[$typeHint])) { $arguments[] = $locator[$typeHint]; continue; } $parameterName = $parameter->getName(); if (isset($locator[$parameterName])) { $arguments[] = $locator[$parameterName]; continue; } $arguments[] = null; } return $arguments; } private function fetchParametersForHandlerFactoryMethod($handlerClassName) { if (! isset($this->tagHandlerParameterCache[$handlerClassName])) { $methodReflection = new \ReflectionMethod($handlerClassName, 'create'); $this->tagHandlerParameterCache[$handlerClassName] = $methodReflection->getParameters(); } return $this->tagHandlerParameterCache[$handlerClassName]; } private function getServiceLocatorWithDynamicParameters(TypeContext $context, $tagName, $tagBody) { $locator = array_merge( $this->serviceLocator, [ 'name' => $tagName, 'body' => $tagBody, TypeContext::class => $context ] ); return $locator; } private function isAnnotation($tagContent) { return false; } } authorName = $authorName; $this->authorEmail = $authorEmail; } public function getAuthorName() { return $this->authorName; } public function getEmail() { return $this->authorEmail; } public function __toString() { return $this->authorName . (strlen($this->authorEmail) ? ' <' . $this->authorEmail . '>' : ''); } public static function create($body) { Assert::string($body); $splitTagContent = preg_match('/^([^\<]*)(?:\<([^\>]*)\>)?$/u', $body, $matches); if (!$splitTagContent) { return null; } $authorName = trim($matches[1]); $email = isset($matches[2]) ? trim($matches[2]) : ''; return new static($authorName, $email); } } name; } public function getDescription() { return $this->description; } public function render(Formatter $formatter = null) { if ($formatter === null) { $formatter = new Formatter\PassthroughFormatter(); } return $formatter->format($this); } } refers = $refers; $this->description = $description; } public static function create( $body, DescriptionFactory $descriptionFactory = null, FqsenResolver $resolver = null, TypeContext $context = null ) { Assert::string($body); Assert::notEmpty($body); $parts = preg_split('/\s+/Su', $body, 2); return new static( $resolver->resolve($parts[0], $context), $descriptionFactory->create(isset($parts[1]) ? $parts[1] : '', $context) ); } public function getReference() { return $this->refers; } public function __toString() { return $this->refers . ($this->description ? ' ' . $this->description->render() : ''); } } version = $version; $this->description = $description; } public static function create($body, DescriptionFactory $descriptionFactory = null, TypeContext $context = null) { Assert::nullOrString($body); if (empty($body)) { return new static(); } $matches = []; if (!preg_match('/^(' . self::REGEX_VECTOR . ')\s*(.+)?$/sux', $body, $matches)) { return new static( null, null !== $descriptionFactory ? $descriptionFactory->create($body, $context) : null ); } return new static( $matches[1], $descriptionFactory->create(isset($matches[2]) ? $matches[2] : '', $context) ); } public function getVersion() { return $this->version; } public function __toString() { return $this->version . ($this->description ? ' ' . $this->description->render() : ''); } } filePath = $filePath; $this->startingLine = $startingLine; $this->lineCount = $lineCount; $this->name = 'example'; if ($description !== null) { $this->description = trim($description); } $this->isURI = $isURI; } public function getContent() { if (null === $this->description) { $filePath = '"' . $this->filePath . '"'; if ($this->isURI) { $filePath = $this->isUriRelative($this->filePath) ? str_replace('%2F', '/', rawurlencode($this->filePath)) :$this->filePath; } return trim($filePath . ' ' . parent::getDescription()); } return $this->description; } public static function create($body) { if (! preg_match('/^(?:\"([^\"]+)\"|(\S+))(?:\s+(.*))?$/sux', $body, $matches)) { return null; } $filePath = null; $fileUri = null; if ('' !== $matches[1]) { $filePath = $matches[1]; } else { $fileUri = $matches[2]; } $startingLine = 1; $lineCount = null; $description = null; if (array_key_exists(3, $matches)) { $description = $matches[3]; if (preg_match('/^([1-9]\d*)(?:\s+((?1))\s*)?(.*)$/sux', $matches[3], $contentMatches)) { $startingLine = (int)$contentMatches[1]; if (isset($contentMatches[2]) && $contentMatches[2] !== '') { $lineCount = (int)$contentMatches[2]; } if (array_key_exists(3, $contentMatches)) { $description = $contentMatches[3]; } } } return new static( $filePath !== null?$filePath:$fileUri, $fileUri !== null, $startingLine, $lineCount, $description ); } public function getFilePath() { return $this->filePath; } public function __toString() { return $this->filePath . ($this->description ? ' ' . $this->description : ''); } private function isUriRelative($uri) { return false === strpos($uri, ':'); } public function getStartingLine() { return $this->startingLine; } public function getLineCount() { return $this->lineCount; } } maxLen = max($this->maxLen, strlen($tag->getName())); } } public function format(Tag $tag) { return '@' . $tag->getName() . str_repeat(' ', $this->maxLen - strlen($tag->getName()) + 1) . (string)$tag; } } getName() . ' ' . (string)$tag); } } validateTagName($name); $this->name = $name; $this->description = $description; } public static function create( $body, $name = '', DescriptionFactory $descriptionFactory = null, TypeContext $context = null ) { Assert::string($body); Assert::stringNotEmpty($name); Assert::notNull($descriptionFactory); $description = $descriptionFactory && $body ? $descriptionFactory->create($body, $context) : null; return new static($name, $description); } public function __toString() { return ($this->description ? $this->description->render() : ''); } private function validateTagName($name) { if (! preg_match('/^' . StandardTagFactory::REGEX_TAGNAME . '$/u', $name)) { throw new \InvalidArgumentException( 'The tag name "' . $name . '" is not wellformed. Tags may only consist of letters, underscores, ' . 'hyphens and backslashes.' ); } } } link = $link; $this->description = $description; } public static function create($body, DescriptionFactory $descriptionFactory = null, TypeContext $context = null) { Assert::string($body); Assert::notNull($descriptionFactory); $parts = preg_split('/\s+/Su', $body, 2); $description = isset($parts[1]) ? $descriptionFactory->create($parts[1], $context) : null; return new static($parts[0], $description); } public function getLink() { return $this->link; } public function __toString() { return $this->link . ($this->description ? ' ' . $this->description->render() : ''); } } methodName = $methodName; $this->arguments = $this->filterArguments($arguments); $this->returnType = $returnType; $this->isStatic = $static; $this->description = $description; } public static function create( $body, TypeResolver $typeResolver = null, DescriptionFactory $descriptionFactory = null, TypeContext $context = null ) { Assert::stringNotEmpty($body); Assert::allNotNull([ $typeResolver, $descriptionFactory ]); if (!preg_match( '/^ # Static keyword # Declares a static method ONLY if type is also present (?: (static) \s+ )? # Return type (?: ( (?:[\w\|_\\\\]*\$this[\w\|_\\\\]*) | (?: (?:[\w\|_\\\\]+) # array notation (?:\[\])* )* ) \s+ )? # Legacy method name (not captured) (?: [\w_]+\(\)\s+ )? # Method name ([\w\|_\\\\]+) # Arguments (?: \(([^\)]*)\) )? \s* # Description (.*) $/sux', $body, $matches )) { return null; } list(, $static, $returnType, $methodName, $arguments, $description) = $matches; $static = $static === 'static'; if ($returnType === '') { $returnType = 'void'; } $returnType = $typeResolver->resolve($returnType, $context); $description = $descriptionFactory->create($description, $context); if (is_string($arguments) && strlen($arguments) > 0) { $arguments = explode(',', $arguments); foreach($arguments as &$argument) { $argument = explode(' ', self::stripRestArg(trim($argument)), 2); if ($argument[0][0] === '$') { $argumentName = substr($argument[0], 1); $argumentType = new Void_(); } else { $argumentType = $typeResolver->resolve($argument[0], $context); $argumentName = ''; if (isset($argument[1])) { $argument[1] = self::stripRestArg($argument[1]); $argumentName = substr($argument[1], 1); } } $argument = [ 'name' => $argumentName, 'type' => $argumentType]; } } else { $arguments = []; } return new static($methodName, $arguments, $returnType, $static, $description); } public function getMethodName() { return $this->methodName; } public function getArguments() { return $this->arguments; } public function isStatic() { return $this->isStatic; } public function getReturnType() { return $this->returnType; } public function __toString() { $arguments = []; foreach ($this->arguments as $argument) { $arguments[] = $argument['type'] . ' $' . $argument['name']; } return trim(($this->isStatic() ? 'static ' : '') . (string)$this->returnType . ' ' . $this->methodName . '(' . implode(', ', $arguments) . ')' . ($this->description ? ' ' . $this->description->render() : '')); } private function filterArguments($arguments) { foreach ($arguments as &$argument) { if (is_string($argument)) { $argument = [ 'name' => $argument ]; } if (! isset($argument['type'])) { $argument['type'] = new Void_(); } $keys = array_keys($argument); if ($keys !== [ 'name', 'type' ]) { throw new \InvalidArgumentException( 'Arguments can only have the "name" and "type" fields, found: ' . var_export($keys, true) ); } } return $arguments; } private static function stripRestArg($argument) { if (strpos($argument, '...') === 0) { $argument = trim(substr($argument, 3)); } return $argument; } } variableName = $variableName; $this->type = $type; $this->isVariadic = $isVariadic; $this->description = $description; } public static function create( $body, TypeResolver $typeResolver = null, DescriptionFactory $descriptionFactory = null, TypeContext $context = null ) { Assert::stringNotEmpty($body); Assert::allNotNull([$typeResolver, $descriptionFactory]); $parts = preg_split('/(\s+)/Su', $body, 3, PREG_SPLIT_DELIM_CAPTURE); $type = null; $variableName = ''; $isVariadic = false; if (isset($parts[0]) && (strlen($parts[0]) > 0) && ($parts[0][0] !== '$')) { $type = $typeResolver->resolve(array_shift($parts), $context); array_shift($parts); } if (isset($parts[0]) && (strlen($parts[0]) > 0) && ($parts[0][0] == '$' || substr($parts[0], 0, 4) === '...$')) { $variableName = array_shift($parts); array_shift($parts); if (substr($variableName, 0, 3) === '...') { $isVariadic = true; $variableName = substr($variableName, 3); } if (substr($variableName, 0, 1) === '$') { $variableName = substr($variableName, 1); } } $description = $descriptionFactory->create(implode('', $parts), $context); return new static($variableName, $type, $isVariadic, $description); } public function getVariableName() { return $this->variableName; } public function getType() { return $this->type; } public function isVariadic() { return $this->isVariadic; } public function __toString() { return ($this->type ? $this->type . ' ' : '') . ($this->isVariadic() ? '...' : '') . '$' . $this->variableName . ($this->description ? ' ' . $this->description : ''); } } variableName = $variableName; $this->type = $type; $this->description = $description; } public static function create( $body, TypeResolver $typeResolver = null, DescriptionFactory $descriptionFactory = null, TypeContext $context = null ) { Assert::stringNotEmpty($body); Assert::allNotNull([$typeResolver, $descriptionFactory]); $parts = preg_split('/(\s+)/Su', $body, 3, PREG_SPLIT_DELIM_CAPTURE); $type = null; $variableName = ''; if (isset($parts[0]) && (strlen($parts[0]) > 0) && ($parts[0][0] !== '$')) { $type = $typeResolver->resolve(array_shift($parts), $context); array_shift($parts); } if (isset($parts[0]) && (strlen($parts[0]) > 0) && ($parts[0][0] == '$')) { $variableName = array_shift($parts); array_shift($parts); if (substr($variableName, 0, 1) === '$') { $variableName = substr($variableName, 1); } } $description = $descriptionFactory->create(implode('', $parts), $context); return new static($variableName, $type, $description); } public function getVariableName() { return $this->variableName; } public function getType() { return $this->type; } public function __toString() { return ($this->type ? $this->type . ' ' : '') . '$' . $this->variableName . ($this->description ? ' ' . $this->description : ''); } } variableName = $variableName; $this->type = $type; $this->description = $description; } public static function create( $body, TypeResolver $typeResolver = null, DescriptionFactory $descriptionFactory = null, TypeContext $context = null ) { Assert::stringNotEmpty($body); Assert::allNotNull([$typeResolver, $descriptionFactory]); $parts = preg_split('/(\s+)/Su', $body, 3, PREG_SPLIT_DELIM_CAPTURE); $type = null; $variableName = ''; if (isset($parts[0]) && (strlen($parts[0]) > 0) && ($parts[0][0] !== '$')) { $type = $typeResolver->resolve(array_shift($parts), $context); array_shift($parts); } if (isset($parts[0]) && (strlen($parts[0]) > 0) && ($parts[0][0] == '$')) { $variableName = array_shift($parts); array_shift($parts); if (substr($variableName, 0, 1) === '$') { $variableName = substr($variableName, 1); } } $description = $descriptionFactory->create(implode('', $parts), $context); return new static($variableName, $type, $description); } public function getVariableName() { return $this->variableName; } public function getType() { return $this->type; } public function __toString() { return ($this->type ? $this->type . ' ' : '') . '$' . $this->variableName . ($this->description ? ' ' . $this->description : ''); } } variableName = $variableName; $this->type = $type; $this->description = $description; } public static function create( $body, TypeResolver $typeResolver = null, DescriptionFactory $descriptionFactory = null, TypeContext $context = null ) { Assert::stringNotEmpty($body); Assert::allNotNull([$typeResolver, $descriptionFactory]); $parts = preg_split('/(\s+)/Su', $body, 3, PREG_SPLIT_DELIM_CAPTURE); $type = null; $variableName = ''; if (isset($parts[0]) && (strlen($parts[0]) > 0) && ($parts[0][0] !== '$')) { $type = $typeResolver->resolve(array_shift($parts), $context); array_shift($parts); } if (isset($parts[0]) && (strlen($parts[0]) > 0) && ($parts[0][0] == '$')) { $variableName = array_shift($parts); array_shift($parts); if (substr($variableName, 0, 1) === '$') { $variableName = substr($variableName, 1); } } $description = $descriptionFactory->create(implode('', $parts), $context); return new static($variableName, $type, $description); } public function getVariableName() { return $this->variableName; } public function getType() { return $this->type; } public function __toString() { return ($this->type ? $this->type . ' ' : '') . '$' . $this->variableName . ($this->description ? ' ' . $this->description : ''); } } fqsen = $fqsen; } public function __toString() { return (string)$this->fqsen; } } uri = $uri; } public function __toString() { return $this->uri; } } type = $type; $this->description = $description; } public static function create( $body, TypeResolver $typeResolver = null, DescriptionFactory $descriptionFactory = null, TypeContext $context = null ) { Assert::string($body); Assert::allNotNull([$typeResolver, $descriptionFactory]); $parts = preg_split('/\s+/Su', $body, 2); $type = $typeResolver->resolve(isset($parts[0]) ? $parts[0] : '', $context); $description = $descriptionFactory->create(isset($parts[1]) ? $parts[1] : '', $context); return new static($type, $description); } public function getType() { return $this->type; } public function __toString() { return $this->type . ' ' . $this->description; } } refers = $refers; $this->description = $description; } public static function create( $body, FqsenResolver $resolver = null, DescriptionFactory $descriptionFactory = null, TypeContext $context = null ) { Assert::string($body); Assert::allNotNull([$resolver, $descriptionFactory]); $parts = preg_split('/\s+/Su', $body, 2); $description = isset($parts[1]) ? $descriptionFactory->create($parts[1], $context) : null; if (preg_match('/\w:\/\/\w/i', $parts[0])) { return new static(new Url($parts[0]), $description); } return new static(new FqsenRef($resolver->resolve($parts[0], $context)), $description); } public function getReference() { return $this->refers; } public function __toString() { return $this->refers . ($this->description ? ' ' . $this->description->render() : ''); } } version = $version; $this->description = $description; } public static function create($body, DescriptionFactory $descriptionFactory = null, TypeContext $context = null) { Assert::nullOrString($body); if (empty($body)) { return new static(); } $matches = []; if (! preg_match('/^(' . self::REGEX_VECTOR . ')\s*(.+)?$/sux', $body, $matches)) { return null; } return new static( $matches[1], $descriptionFactory->create(isset($matches[2]) ? $matches[2] : '', $context) ); } public function getVersion() { return $this->version; } public function __toString() { return $this->version . ($this->description ? ' ' . $this->description->render() : ''); } } startingLine = (int)$startingLine; $this->lineCount = $lineCount !== null ? (int)$lineCount : null; $this->description = $description; } public static function create($body, DescriptionFactory $descriptionFactory = null, TypeContext $context = null) { Assert::stringNotEmpty($body); Assert::notNull($descriptionFactory); $startingLine = 1; $lineCount = null; $description = null; if (preg_match('/^([1-9]\d*)\s*(?:((?1))\s+)?(.*)$/sux', $body, $matches)) { $startingLine = (int)$matches[1]; if (isset($matches[2]) && $matches[2] !== '') { $lineCount = (int)$matches[2]; } $description = $matches[3]; } return new static($startingLine, $lineCount, $descriptionFactory->create($description, $context)); } public function getStartingLine() { return $this->startingLine; } public function getLineCount() { return $this->lineCount; } public function __toString() { return $this->startingLine . ($this->lineCount !== null ? ' ' . $this->lineCount : '') . ($this->description ? ' ' . $this->description->render() : ''); } } type = $type; $this->description = $description; } public static function create( $body, TypeResolver $typeResolver = null, DescriptionFactory $descriptionFactory = null, TypeContext $context = null ) { Assert::string($body); Assert::allNotNull([$typeResolver, $descriptionFactory]); $parts = preg_split('/\s+/Su', $body, 2); $type = $typeResolver->resolve(isset($parts[0]) ? $parts[0] : '', $context); $description = $descriptionFactory->create(isset($parts[1]) ? $parts[1] : '', $context); return new static($type, $description); } public function getType() { return $this->type; } public function __toString() { return $this->type . ' ' . $this->description; } } refers = $refers; $this->description = $description; } public static function create( $body, FqsenResolver $resolver = null, DescriptionFactory $descriptionFactory = null, TypeContext $context = null ) { Assert::string($body); Assert::allNotNull([$resolver, $descriptionFactory]); $parts = preg_split('/\s+/Su', $body, 2); return new static( $resolver->resolve($parts[0], $context), $descriptionFactory->create(isset($parts[1]) ? $parts[1] : '', $context) ); } public function getReference() { return $this->refers; } public function __toString() { return $this->refers . ' ' . $this->description->render(); } } variableName = $variableName; $this->type = $type; $this->description = $description; } public static function create( $body, TypeResolver $typeResolver = null, DescriptionFactory $descriptionFactory = null, TypeContext $context = null ) { Assert::stringNotEmpty($body); Assert::allNotNull([$typeResolver, $descriptionFactory]); $parts = preg_split('/(\s+)/Su', $body, 3, PREG_SPLIT_DELIM_CAPTURE); $type = null; $variableName = ''; if (isset($parts[0]) && (strlen($parts[0]) > 0) && ($parts[0][0] !== '$')) { $type = $typeResolver->resolve(array_shift($parts), $context); array_shift($parts); } if (isset($parts[0]) && (strlen($parts[0]) > 0) && ($parts[0][0] == '$')) { $variableName = array_shift($parts); array_shift($parts); if (substr($variableName, 0, 1) === '$') { $variableName = substr($variableName, 1); } } $description = $descriptionFactory->create(implode('', $parts), $context); return new static($variableName, $type, $description); } public function getVariableName() { return $this->variableName; } public function getType() { return $this->type; } public function __toString() { return ($this->type ? $this->type.' ' : '') .(empty($this->variableName) ? null : ('$'.$this->variableName)) .($this->description ? ' '.$this->description : ''); } } version = $version; $this->description = $description; } public static function create($body, DescriptionFactory $descriptionFactory = null, TypeContext $context = null) { Assert::nullOrString($body); if (empty($body)) { return new static(); } $matches = []; if (!preg_match('/^(' . self::REGEX_VECTOR . ')\s*(.+)?$/sux', $body, $matches)) { return null; } return new static( $matches[1], $descriptionFactory->create(isset($matches[2]) ? $matches[2] : '', $context) ); } public function getVersion() { return $this->version; } public function __toString() { return $this->version . ($this->description ? ' ' . $this->description->render() : ''); } } summary = $summary; $this->description = $description ?: new DocBlock\Description(''); foreach ($tags as $tag) { $this->addTag($tag); } $this->context = $context; $this->location = $location; $this->isTemplateEnd = $isTemplateEnd; $this->isTemplateStart = $isTemplateStart; } public function getSummary() { return $this->summary; } public function getDescription() { return $this->description; } public function getContext() { return $this->context; } public function getLocation() { return $this->location; } public function isTemplateStart() { return $this->isTemplateStart; } public function isTemplateEnd() { return $this->isTemplateEnd; } public function getTags() { return $this->tags; } public function getTagsByName($name) { Assert::string($name); $result = array(); foreach ($this->getTags() as $tag) { if ($tag->getName() != $name) { continue; } $result[] = $tag; } return $result; } public function hasTag($name) { Assert::string($name); foreach ($this->getTags() as $tag) { if ($tag->getName() == $name) { return true; } } return false; } private function addTag(Tag $tag) { $this->tags[] = $tag; } } descriptionFactory = $descriptionFactory; $this->tagFactory = $tagFactory; } public static function createInstance(array $additionalTags = []) { $fqsenResolver = new FqsenResolver(); $tagFactory = new StandardTagFactory($fqsenResolver); $descriptionFactory = new DescriptionFactory($tagFactory); $tagFactory->addService($descriptionFactory); $tagFactory->addService(new TypeResolver($fqsenResolver)); $docBlockFactory = new self($descriptionFactory, $tagFactory); foreach ($additionalTags as $tagName => $tagHandler) { $docBlockFactory->registerTagHandler($tagName, $tagHandler); } return $docBlockFactory; } public function create($docblock, Types\Context $context = null, Location $location = null) { if (is_object($docblock)) { if (!method_exists($docblock, 'getDocComment')) { $exceptionMessage = 'Invalid object passed; the given object must support the getDocComment method'; throw new \InvalidArgumentException($exceptionMessage); } $docblock = $docblock->getDocComment(); } Assert::stringNotEmpty($docblock); if ($context === null) { $context = new Types\Context(''); } $parts = $this->splitDocBlock($this->stripDocComment($docblock)); list($templateMarker, $summary, $description, $tags) = $parts; return new DocBlock( $summary, $description ? $this->descriptionFactory->create($description, $context) : null, array_filter($this->parseTagBlock($tags, $context), function($tag) { return $tag instanceof Tag; }), $context, $location, $templateMarker === '#@+', $templateMarker === '#@-' ); } public function registerTagHandler($tagName, $handler) { $this->tagFactory->registerTagHandler($tagName, $handler); } private function stripDocComment($comment) { $comment = trim(preg_replace('#[ \t]*(?:\/\*\*|\*\/|\*)?[ \t]{0,1}(.*)?#u', '$1', $comment)); if (substr($comment, -2) == '*/') { $comment = trim(substr($comment, 0, -2)); } return str_replace(array("\r\n", "\r"), "\n", $comment); } private function splitDocBlock($comment) { if (strpos($comment, '@') === 0) { return array('', '', '', $comment); } $comment = preg_replace('/\h*$/Sum', '', $comment); preg_match( '/ \A # 1. Extract the template marker (?:(\#\@\+|\#\@\-)\n?)? # 2. Extract the summary (?: (?! @\pL ) # The summary may not start with an @ ( [^\n.]+ (?: (?! \. \n | \n{2} ) # End summary upon a dot followed by newline or two newlines [\n.] (?! [ \t]* @\pL ) # End summary when an @ is found as first character on a new line [^\n.]+ # Include anything else )* \.? )? ) # 3. Extract the description (?: \s* # Some form of whitespace _must_ precede a description because a summary must be there (?! @\pL ) # The description may not start with an @ ( [^\n]+ (?: \n+ (?! [ \t]* @\pL ) # End description when an @ is found as first character on a new line [^\n]+ # Include anything else )* ) )? # 4. Extract the tags (anything that follows) (\s+ [\s\S]*)? # everything that follows /ux', $comment, $matches ); array_shift($matches); while (count($matches) < 4) { $matches[] = ''; } return $matches; } private function parseTagBlock($tags, Types\Context $context) { $tags = $this->filterTagBlock($tags); if (!$tags) { return []; } $result = $this->splitTagBlockIntoTagLines($tags); foreach ($result as $key => $tagLine) { $result[$key] = $this->tagFactory->create(trim($tagLine), $context); } return $result; } private function splitTagBlockIntoTagLines($tags) { $result = array(); foreach (explode("\n", $tags) as $tag_line) { if (isset($tag_line[0]) && ($tag_line[0] === '@')) { $result[] = $tag_line; } else { $result[count($result) - 1] .= "\n" . $tag_line; } } return $result; } private function filterTagBlock($tags) { $tags = trim($tags); if (!$tags) { return null; } if ('@' !== $tags[0]) { throw new \LogicException('A tag block started with text instead of an at-sign(@): ' . $tags); } return $tags; } } isFqsen($fqsen)) { return new Fqsen($fqsen); } return $this->resolvePartialStructuralElementName($fqsen, $context); } private function isFqsen($type) { return strpos($type, self::OPERATOR_NAMESPACE) === 0; } private function resolvePartialStructuralElementName($type, Context $context) { $typeParts = explode(self::OPERATOR_NAMESPACE, $type, 2); $namespaceAliases = $context->getNamespaceAliases(); if (!isset($namespaceAliases[$typeParts[0]])) { $namespace = $context->getNamespace(); if ('' !== $namespace) { $namespace .= self::OPERATOR_NAMESPACE; } return new Fqsen(self::OPERATOR_NAMESPACE . $namespace . $type); } $typeParts[0] = $namespaceAliases[$typeParts[0]]; return new Fqsen(self::OPERATOR_NAMESPACE . implode(self::OPERATOR_NAMESPACE, $typeParts)); } } Types\String_::class, 'int' => Types\Integer::class, 'integer' => Types\Integer::class, 'bool' => Types\Boolean::class, 'boolean' => Types\Boolean::class, 'float' => Types\Float_::class, 'double' => Types\Float_::class, 'object' => Object_::class, 'mixed' => Types\Mixed_::class, 'array' => Array_::class, 'resource' => Types\Resource_::class, 'void' => Types\Void_::class, 'null' => Types\Null_::class, 'scalar' => Types\Scalar::class, 'callback' => Types\Callable_::class, 'callable' => Types\Callable_::class, 'false' => Types\Boolean::class, 'true' => Types\Boolean::class, 'self' => Types\Self_::class, '$this' => Types\This::class, 'static' => Types\Static_::class, 'parent' => Types\Parent_::class, 'iterable' => Iterable_::class, ); private $fqsenResolver; public function __construct(FqsenResolver $fqsenResolver = null) { $this->fqsenResolver = $fqsenResolver ?: new FqsenResolver(); } public function resolve($type, Context $context = null) { if (!is_string($type)) { throw new \InvalidArgumentException( 'Attempted to resolve type but it appeared not to be a string, received: ' . var_export($type, true) ); } $type = trim($type); if (!$type) { throw new \InvalidArgumentException('Attempted to resolve "' . $type . '" but it appears to be empty'); } if ($context === null) { $context = new Context(''); } switch (true) { case $this->isNullableType($type): return $this->resolveNullableType($type, $context); case $this->isKeyword($type): return $this->resolveKeyword($type); case ($this->isCompoundType($type)): return $this->resolveCompoundType($type, $context); case $this->isTypedArray($type): return $this->resolveTypedArray($type, $context); case $this->isFqsen($type): return $this->resolveTypedObject($type); case $this->isPartialStructuralElementName($type): return $this->resolveTypedObject($type, $context); default: throw new \RuntimeException( 'Unable to resolve type "' . $type . '", there is no known method to resolve it' ); } } public function addKeyword($keyword, $typeClassName) { if (!class_exists($typeClassName)) { throw new \InvalidArgumentException( 'The Value Object that needs to be created with a keyword "' . $keyword . '" must be an existing class' . ' but we could not find the class ' . $typeClassName ); } if (!in_array(Type::class, class_implements($typeClassName))) { throw new \InvalidArgumentException( 'The class "' . $typeClassName . '" must implement the interface "phpDocumentor\Reflection\Type"' ); } $this->keywords[$keyword] = $typeClassName; } private function isTypedArray($type) { return substr($type, -2) === self::OPERATOR_ARRAY; } private function isKeyword($type) { return in_array(strtolower($type), array_keys($this->keywords), true); } private function isPartialStructuralElementName($type) { return ($type[0] !== self::OPERATOR_NAMESPACE) && !$this->isKeyword($type); } private function isFqsen($type) { return strpos($type, self::OPERATOR_NAMESPACE) === 0; } private function isCompoundType($type) { return strpos($type, '|') !== false; } private function isNullableType($type) { return $type[0] === '?'; } private function resolveTypedArray($type, Context $context) { return new Array_($this->resolve(substr($type, 0, -2), $context)); } private function resolveKeyword($type) { $className = $this->keywords[strtolower($type)]; return new $className(); } private function resolveTypedObject($type, Context $context = null) { return new Object_($this->fqsenResolver->resolve($type, $context)); } private function resolveCompoundType($type, Context $context) { $types = []; foreach (explode('|', $type) as $part) { $types[] = $this->resolve($part, $context); } return new Compound($types); } private function resolveNullableType($type, Context $context) { return new Nullable($this->resolve(ltrim($type, '?'), $context)); } } valueType = $valueType; $this->keyType = $keyType; } public function getKeyType() { return $this->keyType; } public function getValueType() { return $this->valueType; } public function __toString() { if ($this->valueType instanceof Mixed_) { return 'array'; } return $this->valueType . '[]'; } } types = $types; } public function get($index) { if (!$this->has($index)) { return null; } return $this->types[$index]; } public function has($index) { return isset($this->types[$index]); } public function __toString() { return implode('|', $this->types); } public function getIterator() { return new ArrayIterator($this->types); } } namespace = ('global' !== $namespace && 'default' !== $namespace) ? trim((string)$namespace, '\\') : ''; foreach ($namespaceAliases as $alias => $fqnn) { if ($fqnn[0] === '\\') { $fqnn = substr($fqnn, 1); } if ($fqnn[strlen($fqnn) - 1] === '\\') { $fqnn = substr($fqnn, 0, -1); } $namespaceAliases[$alias] = $fqnn; } $this->namespaceAliases = $namespaceAliases; } public function getNamespace() { return $this->namespace; } public function getNamespaceAliases() { return $this->namespaceAliases; } } getDeclaringClass(); } $fileName = $reflector->getFileName(); $namespace = $reflector->getNamespaceName(); if (file_exists($fileName)) { return $this->createForNamespace($namespace, file_get_contents($fileName)); } return new Context($namespace, []); } public function createForNamespace($namespace, $fileContents) { $namespace = trim($namespace, '\\'); $useStatements = []; $currentNamespace = ''; $tokens = new \ArrayIterator(token_get_all($fileContents)); while ($tokens->valid()) { switch ($tokens->current()[0]) { case T_NAMESPACE: $currentNamespace = $this->parseNamespace($tokens); break; case T_CLASS: $braceLevel = 0; $firstBraceFound = false; while ($tokens->valid() && ($braceLevel > 0 || !$firstBraceFound)) { if ($tokens->current() === '{' || $tokens->current()[0] === T_CURLY_OPEN || $tokens->current()[0] === T_DOLLAR_OPEN_CURLY_BRACES) { if (!$firstBraceFound) { $firstBraceFound = true; } $braceLevel++; } if ($tokens->current() === '}') { $braceLevel--; } $tokens->next(); } break; case T_USE: if ($currentNamespace === $namespace) { $useStatements = array_merge($useStatements, $this->parseUseStatement($tokens)); } break; } $tokens->next(); } return new Context($namespace, $useStatements); } private function parseNamespace(\ArrayIterator $tokens) { $this->skipToNextStringOrNamespaceSeparator($tokens); $name = ''; while ($tokens->valid() && ($tokens->current()[0] === T_STRING || $tokens->current()[0] === T_NS_SEPARATOR) ) { $name .= $tokens->current()[1]; $tokens->next(); } return $name; } private function parseUseStatement(\ArrayIterator $tokens) { $uses = []; $continue = true; while ($continue) { $this->skipToNextStringOrNamespaceSeparator($tokens); list($alias, $fqnn) = $this->extractUseStatement($tokens); $uses[$alias] = $fqnn; if ($tokens->current()[0] === self::T_LITERAL_END_OF_USE) { $continue = false; } } return $uses; } private function skipToNextStringOrNamespaceSeparator(\ArrayIterator $tokens) { while ($tokens->valid() && ($tokens->current()[0] !== T_STRING) && ($tokens->current()[0] !== T_NS_SEPARATOR)) { $tokens->next(); } } private function extractUseStatement(\ArrayIterator $tokens) { $result = ['']; while ($tokens->valid() && ($tokens->current()[0] !== self::T_LITERAL_USE_SEPARATOR) && ($tokens->current()[0] !== self::T_LITERAL_END_OF_USE) ) { if ($tokens->current()[0] === T_AS) { $result[] = ''; } if ($tokens->current()[0] === T_STRING || $tokens->current()[0] === T_NS_SEPARATOR) { $result[count($result) - 1] .= $tokens->current()[1]; } $tokens->next(); } if (count($result) == 1) { $backslashPos = strrpos($result[0], '\\'); if (false !== $backslashPos) { $result[] = substr($result[0], $backslashPos + 1); } else { $result[] = $result[0]; } } return array_reverse($result); } } realType = $realType; } public function getActualType() { return $this->realType; } public function __toString() { return '?' . $this->realType->__toString(); } } fqsen = $fqsen; } public function getFqsen() { return $this->fqsen; } public function __toString() { if ($this->fqsen) { return (string)$this->fqsen; } return 'object'; } } callback = $callback; $this->arguments = $arguments; } public function isDefined() { return $this->option()->isDefined(); } public function isEmpty() { return $this->option()->isEmpty(); } public function get() { return $this->option()->get(); } public function getOrElse($default) { return $this->option()->getOrElse($default); } public function getOrCall($callable) { return $this->option()->getOrCall($callable); } public function getOrThrow(\Exception $ex) { return $this->option()->getOrThrow($ex); } public function orElse(Option $else) { return $this->option()->orElse($else); } public function ifDefined($callable) { $this->option()->ifDefined($callable); } public function forAll($callable) { return $this->option()->forAll($callable); } public function map($callable) { return $this->option()->map($callable); } public function flatMap($callable) { return $this->option()->flatMap($callable); } public function filter($callable) { return $this->option()->filter($callable); } public function filterNot($callable) { return $this->option()->filterNot($callable); } public function select($value) { return $this->option()->select($value); } public function reject($value) { return $this->option()->reject($value); } public function getIterator() { return $this->option()->getIterator(); } public function foldLeft($initialValue, $callable) { return $this->option()->foldLeft($initialValue, $callable); } public function foldRight($initialValue, $callable) { return $this->option()->foldRight($initialValue, $callable); } private function option() { if (null === $this->option) { $this->option = call_user_func_array($this->callback, $this->arguments); if (!$this->option instanceof Option) { $this->option = null; throw new \RuntimeException('Expected instance of \PhpOption\Option'); } } return $this->option; } } value = $value; } public static function create($value) { return new self($value); } public function isDefined() { return true; } public function isEmpty() { return false; } public function get() { return $this->value; } public function getOrElse($default) { return $this->value; } public function getOrCall($callable) { return $this->value; } public function getOrThrow(\Exception $ex) { return $this->value; } public function orElse(Option $else) { return $this; } public function ifDefined($callable) { call_user_func($callable, $this->value); } public function forAll($callable) { call_user_func($callable, $this->value); return $this; } public function map($callable) { return new self(call_user_func($callable, $this->value)); } public function flatMap($callable) { $rs = call_user_func($callable, $this->value); if ( ! $rs instanceof Option) { throw new \RuntimeException('Callables passed to flatMap() must return an Option. Maybe you should use map() instead?'); } return $rs; } public function filter($callable) { if (true === call_user_func($callable, $this->value)) { return $this; } return None::create(); } public function filterNot($callable) { if (false === call_user_func($callable, $this->value)) { return $this; } return None::create(); } public function select($value) { if ($this->value === $value) { return $this; } return None::create(); } public function reject($value) { if ($this->value === $value) { return None::create(); } return $this; } public function getIterator() { return new ArrayIterator(array($this->value)); } public function foldLeft($initialValue, $callable) { return call_user_func($callable, $initialValue, $this->value); } public function foldRight($initialValue, $callable) { return call_user_func($callable, $this->value, $initialValue); } } selectDriver(); } if ($filter === null) { $filter = new Filter; } $this->driver = $driver; $this->filter = $filter; $this->wizard = new Wizard; } public function getReport() { $builder = new Builder; return $builder->build($this); } public function clear() { $this->isInitialized = false; $this->currentId = null; $this->data = []; $this->tests = []; } public function filter() { return $this->filter; } public function getData($raw = false) { if (!$raw && $this->addUncoveredFilesFromWhitelist) { $this->addUncoveredFilesFromWhitelist(); } return $this->data; } public function setData(array $data) { $this->data = $data; } public function getTests() { return $this->tests; } public function setTests(array $tests) { $this->tests = $tests; } public function start($id, $clear = false) { if (!is_bool($clear)) { throw InvalidArgumentException::create( 1, 'boolean' ); } if ($clear) { $this->clear(); } if ($this->isInitialized === false) { $this->initializeData(); } $this->currentId = $id; $this->driver->start($this->shouldCheckForDeadAndUnused); } public function stop($append = true, $linesToBeCovered = [], array $linesToBeUsed = []) { if (!is_bool($append)) { throw InvalidArgumentException::create( 1, 'boolean' ); } if (!is_array($linesToBeCovered) && $linesToBeCovered !== false) { throw InvalidArgumentException::create( 2, 'array or false' ); } $data = $this->driver->stop(); $this->append($data, null, $append, $linesToBeCovered, $linesToBeUsed); $this->currentId = null; return $data; } public function append(array $data, $id = null, $append = true, $linesToBeCovered = [], array $linesToBeUsed = []) { if ($id === null) { $id = $this->currentId; } if ($id === null) { throw new RuntimeException; } $this->applyListsFilter($data); $this->applyIgnoredLinesFilter($data); $this->initializeFilesThatAreSeenTheFirstTime($data); if (!$append) { return; } if ($id != 'UNCOVERED_FILES_FROM_WHITELIST') { $this->applyCoversAnnotationFilter( $data, $linesToBeCovered, $linesToBeUsed ); } if (empty($data)) { return; } $size = 'unknown'; $status = null; if ($id instanceof \PHPUnit_Framework_TestCase) { $_size = $id->getSize(); if ($_size == \PHPUnit_Util_Test::SMALL) { $size = 'small'; } elseif ($_size == \PHPUnit_Util_Test::MEDIUM) { $size = 'medium'; } elseif ($_size == \PHPUnit_Util_Test::LARGE) { $size = 'large'; } $status = $id->getStatus(); $id = get_class($id) . '::' . $id->getName(); } elseif ($id instanceof \PHPUnit_Extensions_PhptTestCase) { $size = 'large'; $id = $id->getName(); } $this->tests[$id] = ['size' => $size, 'status' => $status]; foreach ($data as $file => $lines) { if (!$this->filter->isFile($file)) { continue; } foreach ($lines as $k => $v) { if ($v == Driver::LINE_EXECUTED) { if (empty($this->data[$file][$k]) || !in_array($id, $this->data[$file][$k])) { $this->data[$file][$k][] = $id; } } } } } public function merge(CodeCoverage $that) { $this->filter->setWhitelistedFiles( array_merge($this->filter->getWhitelistedFiles(), $that->filter()->getWhitelistedFiles()) ); foreach ($that->data as $file => $lines) { if (!isset($this->data[$file])) { if (!$this->filter->isFiltered($file)) { $this->data[$file] = $lines; } continue; } foreach ($lines as $line => $data) { if ($data !== null) { if (!isset($this->data[$file][$line])) { $this->data[$file][$line] = $data; } else { $this->data[$file][$line] = array_unique( array_merge($this->data[$file][$line], $data) ); } } } } $this->tests = array_merge($this->tests, $that->getTests()); } public function setCacheTokens($flag) { if (!is_bool($flag)) { throw InvalidArgumentException::create( 1, 'boolean' ); } $this->cacheTokens = $flag; } public function getCacheTokens() { return $this->cacheTokens; } public function setCheckForUnintentionallyCoveredCode($flag) { if (!is_bool($flag)) { throw InvalidArgumentException::create( 1, 'boolean' ); } $this->checkForUnintentionallyCoveredCode = $flag; } public function setForceCoversAnnotation($flag) { if (!is_bool($flag)) { throw InvalidArgumentException::create( 1, 'boolean' ); } $this->forceCoversAnnotation = $flag; } public function setCheckForMissingCoversAnnotation($flag) { if (!is_bool($flag)) { throw InvalidArgumentException::create( 1, 'boolean' ); } $this->checkForMissingCoversAnnotation = $flag; } public function setCheckForUnexecutedCoveredCode($flag) { if (!is_bool($flag)) { throw InvalidArgumentException::create( 1, 'boolean' ); } $this->checkForUnexecutedCoveredCode = $flag; } public function setMapTestClassNameToCoveredClassName($flag) { } public function setAddUncoveredFilesFromWhitelist($flag) { if (!is_bool($flag)) { throw InvalidArgumentException::create( 1, 'boolean' ); } $this->addUncoveredFilesFromWhitelist = $flag; } public function setProcessUncoveredFilesFromWhitelist($flag) { if (!is_bool($flag)) { throw InvalidArgumentException::create( 1, 'boolean' ); } $this->processUncoveredFilesFromWhitelist = $flag; } public function setDisableIgnoredLines($flag) { if (!is_bool($flag)) { throw InvalidArgumentException::create( 1, 'boolean' ); } $this->disableIgnoredLines = $flag; } public function setIgnoreDeprecatedCode($flag) { if (!is_bool($flag)) { throw InvalidArgumentException::create( 1, 'boolean' ); } $this->ignoreDeprecatedCode = $flag; } public function setUnintentionallyCoveredSubclassesWhitelist(array $whitelist) { $this->unintentionallyCoveredSubclassesWhitelist = $whitelist; } private function applyCoversAnnotationFilter(array &$data, $linesToBeCovered, array $linesToBeUsed) { if ($linesToBeCovered === false || ($this->forceCoversAnnotation && empty($linesToBeCovered))) { if ($this->checkForMissingCoversAnnotation) { throw new MissingCoversAnnotationException; } $data = []; return; } if (empty($linesToBeCovered)) { return; } if ($this->checkForUnintentionallyCoveredCode && (!$this->currentId instanceof \PHPUnit_Framework_TestCase || (!$this->currentId->isMedium() && !$this->currentId->isLarge()))) { $this->performUnintentionallyCoveredCodeCheck( $data, $linesToBeCovered, $linesToBeUsed ); } if ($this->checkForUnexecutedCoveredCode) { $this->performUnexecutedCoveredCodeCheck($data, $linesToBeCovered, $linesToBeUsed); } $data = array_intersect_key($data, $linesToBeCovered); foreach (array_keys($data) as $filename) { $_linesToBeCovered = array_flip($linesToBeCovered[$filename]); $data[$filename] = array_intersect_key( $data[$filename], $_linesToBeCovered ); } } private function applyListsFilter(array &$data) { foreach (array_keys($data) as $filename) { if ($this->filter->isFiltered($filename)) { unset($data[$filename]); } } } private function applyIgnoredLinesFilter(array &$data) { foreach (array_keys($data) as $filename) { if (!$this->filter->isFile($filename)) { continue; } foreach ($this->getLinesToBeIgnored($filename) as $line) { unset($data[$filename][$line]); } } } private function initializeFilesThatAreSeenTheFirstTime(array $data) { foreach ($data as $file => $lines) { if ($this->filter->isFile($file) && !isset($this->data[$file])) { $this->data[$file] = []; foreach ($lines as $k => $v) { $this->data[$file][$k] = $v == -2 ? null : []; } } } } private function addUncoveredFilesFromWhitelist() { $data = []; $uncoveredFiles = array_diff( $this->filter->getWhitelist(), array_keys($this->data) ); foreach ($uncoveredFiles as $uncoveredFile) { if (!file_exists($uncoveredFile)) { continue; } if (!$this->processUncoveredFilesFromWhitelist) { $data[$uncoveredFile] = []; $lines = count(file($uncoveredFile)); for ($i = 1; $i <= $lines; $i++) { $data[$uncoveredFile][$i] = Driver::LINE_NOT_EXECUTED; } } } $this->append($data, 'UNCOVERED_FILES_FROM_WHITELIST'); } private function getLinesToBeIgnored($filename) { if (!is_string($filename)) { throw InvalidArgumentException::create( 1, 'string' ); } if (!isset($this->ignoredLines[$filename])) { $this->ignoredLines[$filename] = []; if ($this->disableIgnoredLines) { return $this->ignoredLines[$filename]; } $ignore = false; $stop = false; $lines = file($filename); $numLines = count($lines); foreach ($lines as $index => $line) { if (!trim($line)) { $this->ignoredLines[$filename][] = $index + 1; } } if ($this->cacheTokens) { $tokens = \PHP_Token_Stream_CachingFactory::get($filename); } else { $tokens = new \PHP_Token_Stream($filename); } $classes = array_merge($tokens->getClasses(), $tokens->getTraits()); $tokens = $tokens->tokens(); foreach ($tokens as $token) { switch (get_class($token)) { case 'PHP_Token_COMMENT': case 'PHP_Token_DOC_COMMENT': $_token = trim($token); $_line = trim($lines[$token->getLine() - 1]); if ($_token == '// @codeCoverageIgnore' || $_token == '//@codeCoverageIgnore') { $ignore = true; $stop = true; } elseif ($_token == '// @codeCoverageIgnoreStart' || $_token == '//@codeCoverageIgnoreStart') { $ignore = true; } elseif ($_token == '// @codeCoverageIgnoreEnd' || $_token == '//@codeCoverageIgnoreEnd') { $stop = true; } if (!$ignore) { $start = $token->getLine(); $end = $start + substr_count($token, "\n"); if (0 !== strpos($_token, $_line)) { $start++; } for ($i = $start; $i < $end; $i++) { $this->ignoredLines[$filename][] = $i; } if (isset($lines[$i-1]) && 0 === strpos($_token, '/*') && '*/' === substr(trim($lines[$i-1]), -2)) { $this->ignoredLines[$filename][] = $i; } } break; case 'PHP_Token_INTERFACE': case 'PHP_Token_TRAIT': case 'PHP_Token_CLASS': case 'PHP_Token_FUNCTION': $docblock = $token->getDocblock(); $this->ignoredLines[$filename][] = $token->getLine(); if (strpos($docblock, '@codeCoverageIgnore') || ($this->ignoreDeprecatedCode && strpos($docblock, '@deprecated'))) { $endLine = $token->getEndLine(); for ($i = $token->getLine(); $i <= $endLine; $i++) { $this->ignoredLines[$filename][] = $i; } } elseif ($token instanceof \PHP_Token_INTERFACE || $token instanceof \PHP_Token_TRAIT || $token instanceof \PHP_Token_CLASS) { if (empty($classes[$token->getName()]['methods'])) { for ($i = $token->getLine(); $i <= $token->getEndLine(); $i++) { $this->ignoredLines[$filename][] = $i; } } else { $firstMethod = array_shift( $classes[$token->getName()]['methods'] ); do { $lastMethod = array_pop( $classes[$token->getName()]['methods'] ); } while ($lastMethod !== null && substr($lastMethod['signature'], 0, 18) == 'anonymous function'); if ($lastMethod === null) { $lastMethod = $firstMethod; } for ($i = $token->getLine(); $i < $firstMethod['startLine']; $i++) { $this->ignoredLines[$filename][] = $i; } for ($i = $token->getEndLine(); $i > $lastMethod['endLine']; $i--) { $this->ignoredLines[$filename][] = $i; } } } break; case 'PHP_Token_NAMESPACE': $this->ignoredLines[$filename][] = $token->getEndLine(); case 'PHP_Token_DECLARE': case 'PHP_Token_OPEN_TAG': case 'PHP_Token_CLOSE_TAG': case 'PHP_Token_USE': $this->ignoredLines[$filename][] = $token->getLine(); break; } if ($ignore) { $this->ignoredLines[$filename][] = $token->getLine(); if ($stop) { $ignore = false; $stop = false; } } } $this->ignoredLines[$filename][] = $numLines + 1; $this->ignoredLines[$filename] = array_unique( $this->ignoredLines[$filename] ); sort($this->ignoredLines[$filename]); } return $this->ignoredLines[$filename]; } private function performUnintentionallyCoveredCodeCheck(array &$data, array $linesToBeCovered, array $linesToBeUsed) { $allowedLines = $this->getAllowedLines( $linesToBeCovered, $linesToBeUsed ); $unintentionallyCoveredUnits = []; foreach ($data as $file => $_data) { foreach ($_data as $line => $flag) { if ($flag == 1 && !isset($allowedLines[$file][$line])) { $unintentionallyCoveredUnits[] = $this->wizard->lookup($file, $line); } } } $unintentionallyCoveredUnits = $this->processUnintentionallyCoveredUnits($unintentionallyCoveredUnits); if (!empty($unintentionallyCoveredUnits)) { throw new UnintentionallyCoveredCodeException( $unintentionallyCoveredUnits ); } } private function performUnexecutedCoveredCodeCheck(array &$data, array $linesToBeCovered, array $linesToBeUsed) { $expectedLines = $this->getAllowedLines( $linesToBeCovered, $linesToBeUsed ); foreach ($data as $file => $_data) { foreach (array_keys($_data) as $line) { if (!isset($expectedLines[$file][$line])) { continue; } unset($expectedLines[$file][$line]); } } $message = ''; foreach ($expectedLines as $file => $lines) { if (empty($lines)) { continue; } foreach (array_keys($lines) as $line) { $message .= sprintf('- %s:%d' . PHP_EOL, $file, $line); } } if (!empty($message)) { throw new CoveredCodeNotExecutedException($message); } } private function getAllowedLines(array $linesToBeCovered, array $linesToBeUsed) { $allowedLines = []; foreach (array_keys($linesToBeCovered) as $file) { if (!isset($allowedLines[$file])) { $allowedLines[$file] = []; } $allowedLines[$file] = array_merge( $allowedLines[$file], $linesToBeCovered[$file] ); } foreach (array_keys($linesToBeUsed) as $file) { if (!isset($allowedLines[$file])) { $allowedLines[$file] = []; } $allowedLines[$file] = array_merge( $allowedLines[$file], $linesToBeUsed[$file] ); } foreach (array_keys($allowedLines) as $file) { $allowedLines[$file] = array_flip( array_unique($allowedLines[$file]) ); } return $allowedLines; } private function selectDriver() { $runtime = new Runtime; if (!$runtime->canCollectCodeCoverage()) { throw new RuntimeException('No code coverage driver available'); } if ($runtime->isHHVM()) { return new HHVM; } elseif ($runtime->isPHPDBG()) { return new PHPDBG; } else { return new Xdebug; } } private function processUnintentionallyCoveredUnits(array $unintentionallyCoveredUnits) { $unintentionallyCoveredUnits = array_unique($unintentionallyCoveredUnits); sort($unintentionallyCoveredUnits); foreach (array_keys($unintentionallyCoveredUnits) as $k => $v) { $unit = explode('::', $unintentionallyCoveredUnits[$k]); if (count($unit) != 2) { continue; } $class = new \ReflectionClass($unit[0]); foreach ($this->unintentionallyCoveredSubclassesWhitelist as $whitelisted) { if ($class->isSubclassOf($whitelisted)) { unset($unintentionallyCoveredUnits[$k]); break; } } } return array_values($unintentionallyCoveredUnits); } protected function initializeData() { $this->isInitialized = true; if ($this->processUncoveredFilesFromWhitelist) { $this->shouldCheckForDeadAndUnused = false; $this->driver->start(true); foreach ($this->filter->getWhitelist() as $file) { if ($this->filter->isFile($file)) { include_once($file); } } $data = []; $coverage = $this->driver->stop(); foreach ($coverage as $file => $fileCoverage) { if ($this->filter->isFiltered($file)) { continue; } foreach (array_keys($fileCoverage) as $key) { if ($fileCoverage[$key] == Driver::LINE_EXECUTED) { $fileCoverage[$key] = Driver::LINE_NOT_EXECUTED; } } $data[$file] = $fileCoverage; } $this->append($data, 'UNCOVERED_FILES_FROM_WHITELIST'); } } } $newFiles] ); } else { $sourceLines = []; } } foreach ($sourceLines as $file => $lines) { foreach ($lines as $lineNo => $numExecuted) { $sourceLines[$file][$lineNo] = self::LINE_NOT_EXECUTED; } } $fetchedLines = array_merge($fetchedLines, $sourceLines); return $this->detectExecutedLines($fetchedLines, $dbgData); } private function detectExecutedLines(array $sourceLines, array $dbgData) { foreach ($dbgData as $file => $coveredLines) { foreach ($coveredLines as $lineNo => $numExecuted) { if (isset($sourceLines[$file][$lineNo])) { $sourceLines[$file][$lineNo] = self::LINE_EXECUTED; } } } return $sourceLines; } } =') && !ini_get('xdebug.coverage_enable')) { throw new RuntimeException( 'xdebug.coverage_enable=On has to be set in php.ini' ); } } public function start($determineUnusedAndDead = true) { if ($determineUnusedAndDead) { xdebug_start_code_coverage(XDEBUG_CC_UNUSED | XDEBUG_CC_DEAD_CODE); } else { xdebug_start_code_coverage(); } } public function stop() { $data = xdebug_get_code_coverage(); xdebug_stop_code_coverage(); return $this->cleanup($data); } private function cleanup(array $data) { foreach (array_keys($data) as $file) { unset($data[$file][0]); if (strpos($file, 'xdebug://debug-eval') !== 0 && file_exists($file)) { $numLines = $this->getNumberOfLinesInFile($file); foreach (array_keys($data[$file]) as $line) { if ($line > $numLines) { unset($data[$file][$line]); } } } } return $data; } private function getNumberOfLinesInFile($file) { if (!isset($this->cacheNumLines[$file])) { $buffer = file_get_contents($file); $lines = substr_count($buffer, "\n"); if (substr($buffer, -1) !== "\n") { $lines++; } $this->cacheNumLines[$file] = $lines; } return $this->cacheNumLines[$file]; } } unintentionallyCoveredUnits = $unintentionallyCoveredUnits; parent::__construct($this->toString()); } public function getUnintentionallyCoveredUnits() { return $this->unintentionallyCoveredUnits; } private function toString() { $message = ''; foreach ($this->unintentionallyCoveredUnits as $unit) { $message .= '- ' . $unit . "\n"; } return $message; } } getFilesAsArray($directory, $suffix, $prefix); foreach ($files as $file) { $this->addFileToWhitelist($file); } } public function addFileToWhitelist($filename) { $this->whitelistedFiles[realpath($filename)] = true; } public function addFilesToWhitelist(array $files) { foreach ($files as $file) { $this->addFileToWhitelist($file); } } public function removeDirectoryFromWhitelist($directory, $suffix = '.php', $prefix = '') { $facade = new \File_Iterator_Facade; $files = $facade->getFilesAsArray($directory, $suffix, $prefix); foreach ($files as $file) { $this->removeFileFromWhitelist($file); } } public function removeFileFromWhitelist($filename) { $filename = realpath($filename); unset($this->whitelistedFiles[$filename]); } public function isFile($filename) { if ($filename == '-' || strpos($filename, 'vfs://') === 0 || strpos($filename, 'xdebug://debug-eval') !== false || strpos($filename, 'eval()\'d code') !== false || strpos($filename, 'runtime-created function') !== false || strpos($filename, 'runkit created function') !== false || strpos($filename, 'assert code') !== false || strpos($filename, 'regexp code') !== false) { return false; } return file_exists($filename); } public function isFiltered($filename) { if (!$this->isFile($filename)) { return true; } $filename = realpath($filename); return !isset($this->whitelistedFiles[$filename]); } public function getWhitelist() { return array_keys($this->whitelistedFiles); } public function hasWhitelist() { return !empty($this->whitelistedFiles); } public function getWhitelistedFiles() { return $this->whitelistedFiles; } public function setWhitelistedFiles($whitelistedFiles) { $this->whitelistedFiles = $whitelistedFiles; } } name = $name; $this->parent = $parent; } public function getName() { return $this->name; } public function getId() { if ($this->id === null) { $parent = $this->getParent(); if ($parent === null) { $this->id = 'index'; } else { $parentId = $parent->getId(); if ($parentId == 'index') { $this->id = str_replace(':', '_', $this->name); } else { $this->id = $parentId . '/' . $this->name; } } } return $this->id; } public function getPath() { if ($this->path === null) { if ($this->parent === null || $this->parent->getPath() === null || $this->parent->getPath() === false) { $this->path = $this->name; } else { $this->path = $this->parent->getPath() . '/' . $this->name; } } return $this->path; } public function getPathAsArray() { if ($this->pathArray === null) { if ($this->parent === null) { $this->pathArray = []; } else { $this->pathArray = $this->parent->getPathAsArray(); } $this->pathArray[] = $this; } return $this->pathArray; } public function getParent() { return $this->parent; } public function getTestedClassesPercent($asString = true) { return Util::percent( $this->getNumTestedClasses(), $this->getNumClasses(), $asString ); } public function getTestedTraitsPercent($asString = true) { return Util::percent( $this->getNumTestedTraits(), $this->getNumTraits(), $asString ); } public function getTestedClassesAndTraitsPercent($asString = true) { return Util::percent( $this->getNumTestedClassesAndTraits(), $this->getNumClassesAndTraits(), $asString ); } public function getTestedMethodsPercent($asString = true) { return Util::percent( $this->getNumTestedMethods(), $this->getNumMethods(), $asString ); } public function getLineExecutedPercent($asString = true) { return Util::percent( $this->getNumExecutedLines(), $this->getNumExecutableLines(), $asString ); } public function getNumClassesAndTraits() { return $this->getNumClasses() + $this->getNumTraits(); } public function getNumTestedClassesAndTraits() { return $this->getNumTestedClasses() + $this->getNumTestedTraits(); } public function getClassesAndTraits() { return array_merge($this->getClasses(), $this->getTraits()); } abstract public function getClasses(); abstract public function getTraits(); abstract public function getFunctions(); abstract public function getLinesOfCode(); abstract public function getNumExecutableLines(); abstract public function getNumExecutedLines(); abstract public function getNumClasses(); abstract public function getNumTestedClasses(); abstract public function getNumTraits(); abstract public function getNumTestedTraits(); abstract public function getNumMethods(); abstract public function getNumTestedMethods(); abstract public function getNumFunctions(); abstract public function getNumTestedFunctions(); } getData(); $commonPath = $this->reducePaths($files); $root = new Directory( $commonPath, null ); $this->addItems( $root, $this->buildDirectoryStructure($files), $coverage->getTests(), $coverage->getCacheTokens() ); return $root; } private function addItems(Directory $root, array $items, array $tests, $cacheTokens) { foreach ($items as $key => $value) { if (substr($key, -2) == '/f') { $key = substr($key, 0, -2); if (file_exists($root->getPath() . DIRECTORY_SEPARATOR . $key)) { $root->addFile($key, $value, $tests, $cacheTokens); } } else { $child = $root->addDirectory($key); $this->addItems($child, $value, $tests, $cacheTokens); } } } private function buildDirectoryStructure($files) { $result = []; foreach ($files as $path => $file) { $path = explode('/', $path); $pointer = &$result; $max = count($path); for ($i = 0; $i < $max; $i++) { if ($i == ($max - 1)) { $type = '/f'; } else { $type = ''; } $pointer = &$pointer[$path[$i] . $type]; } $pointer = $file; } return $result; } private function reducePaths(&$files) { if (empty($files)) { return '.'; } $commonPath = ''; $paths = array_keys($files); if (count($files) == 1) { $commonPath = dirname($paths[0]) . '/'; $files[basename($paths[0])] = $files[$paths[0]]; unset($files[$paths[0]]); return $commonPath; } $max = count($paths); for ($i = 0; $i < $max; $i++) { if (strpos($paths[$i], 'phar://') === 0) { $paths[$i] = substr($paths[$i], 7); $paths[$i] = strtr($paths[$i], '/', DIRECTORY_SEPARATOR); } $paths[$i] = explode(DIRECTORY_SEPARATOR, $paths[$i]); if (empty($paths[$i][0])) { $paths[$i][0] = DIRECTORY_SEPARATOR; } } $done = false; $max = count($paths); while (!$done) { for ($i = 0; $i < $max - 1; $i++) { if (!isset($paths[$i][0]) || !isset($paths[$i+1][0]) || $paths[$i][0] != $paths[$i+1][0]) { $done = true; break; } } if (!$done) { $commonPath .= $paths[0][0]; if ($paths[0][0] != DIRECTORY_SEPARATOR) { $commonPath .= DIRECTORY_SEPARATOR; } for ($i = 0; $i < $max; $i++) { array_shift($paths[$i]); } } } $original = array_keys($files); $max = count($original); for ($i = 0; $i < $max; $i++) { $files[implode('/', $paths[$i])] = $files[$original[$i]]; unset($files[$original[$i]]); } ksort($files); return substr($commonPath, 0, -1); } } numFiles == -1) { $this->numFiles = 0; foreach ($this->children as $child) { $this->numFiles += count($child); } } return $this->numFiles; } public function getIterator() { return new \RecursiveIteratorIterator( new Iterator($this), \RecursiveIteratorIterator::SELF_FIRST ); } public function addDirectory($name) { $directory = new self($name, $this); $this->children[] = $directory; $this->directories[] = &$this->children[count($this->children) - 1]; return $directory; } public function addFile($name, array $coverageData, array $testData, $cacheTokens) { $file = new File( $name, $this, $coverageData, $testData, $cacheTokens ); $this->children[] = $file; $this->files[] = &$this->children[count($this->children) - 1]; $this->numExecutableLines = -1; $this->numExecutedLines = -1; return $file; } public function getDirectories() { return $this->directories; } public function getFiles() { return $this->files; } public function getChildNodes() { return $this->children; } public function getClasses() { if ($this->classes === null) { $this->classes = []; foreach ($this->children as $child) { $this->classes = array_merge( $this->classes, $child->getClasses() ); } } return $this->classes; } public function getTraits() { if ($this->traits === null) { $this->traits = []; foreach ($this->children as $child) { $this->traits = array_merge( $this->traits, $child->getTraits() ); } } return $this->traits; } public function getFunctions() { if ($this->functions === null) { $this->functions = []; foreach ($this->children as $child) { $this->functions = array_merge( $this->functions, $child->getFunctions() ); } } return $this->functions; } public function getLinesOfCode() { if ($this->linesOfCode === null) { $this->linesOfCode = ['loc' => 0, 'cloc' => 0, 'ncloc' => 0]; foreach ($this->children as $child) { $linesOfCode = $child->getLinesOfCode(); $this->linesOfCode['loc'] += $linesOfCode['loc']; $this->linesOfCode['cloc'] += $linesOfCode['cloc']; $this->linesOfCode['ncloc'] += $linesOfCode['ncloc']; } } return $this->linesOfCode; } public function getNumExecutableLines() { if ($this->numExecutableLines == -1) { $this->numExecutableLines = 0; foreach ($this->children as $child) { $this->numExecutableLines += $child->getNumExecutableLines(); } } return $this->numExecutableLines; } public function getNumExecutedLines() { if ($this->numExecutedLines == -1) { $this->numExecutedLines = 0; foreach ($this->children as $child) { $this->numExecutedLines += $child->getNumExecutedLines(); } } return $this->numExecutedLines; } public function getNumClasses() { if ($this->numClasses == -1) { $this->numClasses = 0; foreach ($this->children as $child) { $this->numClasses += $child->getNumClasses(); } } return $this->numClasses; } public function getNumTestedClasses() { if ($this->numTestedClasses == -1) { $this->numTestedClasses = 0; foreach ($this->children as $child) { $this->numTestedClasses += $child->getNumTestedClasses(); } } return $this->numTestedClasses; } public function getNumTraits() { if ($this->numTraits == -1) { $this->numTraits = 0; foreach ($this->children as $child) { $this->numTraits += $child->getNumTraits(); } } return $this->numTraits; } public function getNumTestedTraits() { if ($this->numTestedTraits == -1) { $this->numTestedTraits = 0; foreach ($this->children as $child) { $this->numTestedTraits += $child->getNumTestedTraits(); } } return $this->numTestedTraits; } public function getNumMethods() { if ($this->numMethods == -1) { $this->numMethods = 0; foreach ($this->children as $child) { $this->numMethods += $child->getNumMethods(); } } return $this->numMethods; } public function getNumTestedMethods() { if ($this->numTestedMethods == -1) { $this->numTestedMethods = 0; foreach ($this->children as $child) { $this->numTestedMethods += $child->getNumTestedMethods(); } } return $this->numTestedMethods; } public function getNumFunctions() { if ($this->numFunctions == -1) { $this->numFunctions = 0; foreach ($this->children as $child) { $this->numFunctions += $child->getNumFunctions(); } } return $this->numFunctions; } public function getNumTestedFunctions() { if ($this->numTestedFunctions == -1) { $this->numTestedFunctions = 0; foreach ($this->children as $child) { $this->numTestedFunctions += $child->getNumTestedFunctions(); } } return $this->numTestedFunctions; } } coverageData = $coverageData; $this->testData = $testData; $this->cacheTokens = $cacheTokens; $this->calculateStatistics(); } public function count() { return 1; } public function getCoverageData() { return $this->coverageData; } public function getTestData() { return $this->testData; } public function getClasses() { return $this->classes; } public function getTraits() { return $this->traits; } public function getFunctions() { return $this->functions; } public function getLinesOfCode() { return $this->linesOfCode; } public function getNumExecutableLines() { return $this->numExecutableLines; } public function getNumExecutedLines() { return $this->numExecutedLines; } public function getNumClasses() { if ($this->numClasses === null) { $this->numClasses = 0; foreach ($this->classes as $class) { foreach ($class['methods'] as $method) { if ($method['executableLines'] > 0) { $this->numClasses++; continue 2; } } } } return $this->numClasses; } public function getNumTestedClasses() { return $this->numTestedClasses; } public function getNumTraits() { if ($this->numTraits === null) { $this->numTraits = 0; foreach ($this->traits as $trait) { foreach ($trait['methods'] as $method) { if ($method['executableLines'] > 0) { $this->numTraits++; continue 2; } } } } return $this->numTraits; } public function getNumTestedTraits() { return $this->numTestedTraits; } public function getNumMethods() { if ($this->numMethods === null) { $this->numMethods = 0; foreach ($this->classes as $class) { foreach ($class['methods'] as $method) { if ($method['executableLines'] > 0) { $this->numMethods++; } } } foreach ($this->traits as $trait) { foreach ($trait['methods'] as $method) { if ($method['executableLines'] > 0) { $this->numMethods++; } } } } return $this->numMethods; } public function getNumTestedMethods() { if ($this->numTestedMethods === null) { $this->numTestedMethods = 0; foreach ($this->classes as $class) { foreach ($class['methods'] as $method) { if ($method['executableLines'] > 0 && $method['coverage'] == 100) { $this->numTestedMethods++; } } } foreach ($this->traits as $trait) { foreach ($trait['methods'] as $method) { if ($method['executableLines'] > 0 && $method['coverage'] == 100) { $this->numTestedMethods++; } } } } return $this->numTestedMethods; } public function getNumFunctions() { return count($this->functions); } public function getNumTestedFunctions() { if ($this->numTestedFunctions === null) { $this->numTestedFunctions = 0; foreach ($this->functions as $function) { if ($function['executableLines'] > 0 && $function['coverage'] == 100) { $this->numTestedFunctions++; } } } return $this->numTestedFunctions; } protected function calculateStatistics() { $classStack = $functionStack = []; if ($this->cacheTokens) { $tokens = \PHP_Token_Stream_CachingFactory::get($this->getPath()); } else { $tokens = new \PHP_Token_Stream($this->getPath()); } $this->processClasses($tokens); $this->processTraits($tokens); $this->processFunctions($tokens); $this->linesOfCode = $tokens->getLinesOfCode(); unset($tokens); for ($lineNumber = 1; $lineNumber <= $this->linesOfCode['loc']; $lineNumber++) { if (isset($this->startLines[$lineNumber])) { if (isset($this->startLines[$lineNumber]['className'])) { if (isset($currentClass)) { $classStack[] = &$currentClass; } $currentClass = &$this->startLines[$lineNumber]; } elseif (isset($this->startLines[$lineNumber]['traitName'])) { $currentTrait = &$this->startLines[$lineNumber]; } elseif (isset($this->startLines[$lineNumber]['methodName'])) { $currentMethod = &$this->startLines[$lineNumber]; } elseif (isset($this->startLines[$lineNumber]['functionName'])) { if (isset($currentFunction)) { $functionStack[] = &$currentFunction; } $currentFunction = &$this->startLines[$lineNumber]; } } if (isset($this->coverageData[$lineNumber])) { if (isset($currentClass)) { $currentClass['executableLines']++; } if (isset($currentTrait)) { $currentTrait['executableLines']++; } if (isset($currentMethod)) { $currentMethod['executableLines']++; } if (isset($currentFunction)) { $currentFunction['executableLines']++; } $this->numExecutableLines++; if (count($this->coverageData[$lineNumber]) > 0) { if (isset($currentClass)) { $currentClass['executedLines']++; } if (isset($currentTrait)) { $currentTrait['executedLines']++; } if (isset($currentMethod)) { $currentMethod['executedLines']++; } if (isset($currentFunction)) { $currentFunction['executedLines']++; } $this->numExecutedLines++; } } if (isset($this->endLines[$lineNumber])) { if (isset($this->endLines[$lineNumber]['className'])) { unset($currentClass); if ($classStack) { end($classStack); $key = key($classStack); $currentClass = &$classStack[$key]; unset($classStack[$key]); } } elseif (isset($this->endLines[$lineNumber]['traitName'])) { unset($currentTrait); } elseif (isset($this->endLines[$lineNumber]['methodName'])) { unset($currentMethod); } elseif (isset($this->endLines[$lineNumber]['functionName'])) { unset($currentFunction); if ($functionStack) { end($functionStack); $key = key($functionStack); $currentFunction = &$functionStack[$key]; unset($functionStack[$key]); } } } } foreach ($this->traits as &$trait) { foreach ($trait['methods'] as &$method) { if ($method['executableLines'] > 0) { $method['coverage'] = ($method['executedLines'] / $method['executableLines']) * 100; } else { $method['coverage'] = 100; } $method['crap'] = $this->crap( $method['ccn'], $method['coverage'] ); $trait['ccn'] += $method['ccn']; } if ($trait['executableLines'] > 0) { $trait['coverage'] = ($trait['executedLines'] / $trait['executableLines']) * 100; if ($trait['coverage'] == 100) { $this->numTestedClasses++; } } else { $trait['coverage'] = 100; } $trait['crap'] = $this->crap( $trait['ccn'], $trait['coverage'] ); } foreach ($this->classes as &$class) { foreach ($class['methods'] as &$method) { if ($method['executableLines'] > 0) { $method['coverage'] = ($method['executedLines'] / $method['executableLines']) * 100; } else { $method['coverage'] = 100; } $method['crap'] = $this->crap( $method['ccn'], $method['coverage'] ); $class['ccn'] += $method['ccn']; } if ($class['executableLines'] > 0) { $class['coverage'] = ($class['executedLines'] / $class['executableLines']) * 100; if ($class['coverage'] == 100) { $this->numTestedClasses++; } } else { $class['coverage'] = 100; } $class['crap'] = $this->crap( $class['ccn'], $class['coverage'] ); } } protected function processClasses(\PHP_Token_Stream $tokens) { $classes = $tokens->getClasses(); unset($tokens); $link = $this->getId() . '.html#'; foreach ($classes as $className => $class) { $this->classes[$className] = [ 'className' => $className, 'methods' => [], 'startLine' => $class['startLine'], 'executableLines' => 0, 'executedLines' => 0, 'ccn' => 0, 'coverage' => 0, 'crap' => 0, 'package' => $class['package'], 'link' => $link . $class['startLine'] ]; $this->startLines[$class['startLine']] = &$this->classes[$className]; $this->endLines[$class['endLine']] = &$this->classes[$className]; foreach ($class['methods'] as $methodName => $method) { $this->classes[$className]['methods'][$methodName] = $this->newMethod($methodName, $method, $link); $this->startLines[$method['startLine']] = &$this->classes[$className]['methods'][$methodName]; $this->endLines[$method['endLine']] = &$this->classes[$className]['methods'][$methodName]; } } } protected function processTraits(\PHP_Token_Stream $tokens) { $traits = $tokens->getTraits(); unset($tokens); $link = $this->getId() . '.html#'; foreach ($traits as $traitName => $trait) { $this->traits[$traitName] = [ 'traitName' => $traitName, 'methods' => [], 'startLine' => $trait['startLine'], 'executableLines' => 0, 'executedLines' => 0, 'ccn' => 0, 'coverage' => 0, 'crap' => 0, 'package' => $trait['package'], 'link' => $link . $trait['startLine'] ]; $this->startLines[$trait['startLine']] = &$this->traits[$traitName]; $this->endLines[$trait['endLine']] = &$this->traits[$traitName]; foreach ($trait['methods'] as $methodName => $method) { $this->traits[$traitName]['methods'][$methodName] = $this->newMethod($methodName, $method, $link); $this->startLines[$method['startLine']] = &$this->traits[$traitName]['methods'][$methodName]; $this->endLines[$method['endLine']] = &$this->traits[$traitName]['methods'][$methodName]; } } } protected function processFunctions(\PHP_Token_Stream $tokens) { $functions = $tokens->getFunctions(); unset($tokens); $link = $this->getId() . '.html#'; foreach ($functions as $functionName => $function) { $this->functions[$functionName] = [ 'functionName' => $functionName, 'signature' => $function['signature'], 'startLine' => $function['startLine'], 'executableLines' => 0, 'executedLines' => 0, 'ccn' => $function['ccn'], 'coverage' => 0, 'crap' => 0, 'link' => $link . $function['startLine'] ]; $this->startLines[$function['startLine']] = &$this->functions[$functionName]; $this->endLines[$function['endLine']] = &$this->functions[$functionName]; } } protected function crap($ccn, $coverage) { if ($coverage == 0) { return (string) (pow($ccn, 2) + $ccn); } if ($coverage >= 95) { return (string) $ccn; } return sprintf( '%01.2F', pow($ccn, 2) * pow(1 - $coverage/100, 3) + $ccn ); } private function newMethod($methodName, array $method, $link) { return [ 'methodName' => $methodName, 'visibility' => $method['visibility'], 'signature' => $method['signature'], 'startLine' => $method['startLine'], 'endLine' => $method['endLine'], 'executableLines' => 0, 'executedLines' => 0, 'ccn' => $method['ccn'], 'coverage' => 0, 'crap' => 0, 'link' => $link . $method['startLine'], ]; } } nodes = $node->getChildNodes(); } public function rewind() { $this->position = 0; } public function valid() { return $this->position < count($this->nodes); } public function key() { return $this->position; } public function current() { return $this->valid() ? $this->nodes[$this->position] : null; } public function next() { $this->position++; } public function getChildren() { return new self( $this->nodes[$this->position] ); } public function hasChildren() { return $this->nodes[$this->position] instanceof Directory; } } formatOutput = true; $xmlCoverage = $xmlDocument->createElement('coverage'); $xmlCoverage->setAttribute('generated', (int) $_SERVER['REQUEST_TIME']); $xmlDocument->appendChild($xmlCoverage); $xmlProject = $xmlDocument->createElement('project'); $xmlProject->setAttribute('timestamp', (int) $_SERVER['REQUEST_TIME']); if (is_string($name)) { $xmlProject->setAttribute('name', $name); } $xmlCoverage->appendChild($xmlProject); $packages = []; $report = $coverage->getReport(); unset($coverage); foreach ($report as $item) { if (!$item instanceof File) { continue; } $xmlFile = $xmlDocument->createElement('file'); $xmlFile->setAttribute('name', $item->getPath()); $classes = $item->getClassesAndTraits(); $coverage = $item->getCoverageData(); $lines = []; $namespace = 'global'; foreach ($classes as $className => $class) { $classStatements = 0; $coveredClassStatements = 0; $coveredMethods = 0; $classMethods = 0; foreach ($class['methods'] as $methodName => $method) { if ($method['executableLines'] == 0) { continue; } $classMethods++; $classStatements += $method['executableLines']; $coveredClassStatements += $method['executedLines']; if ($method['coverage'] == 100) { $coveredMethods++; } $methodCount = 0; foreach (range($method['startLine'], $method['endLine']) as $line) { if (isset($coverage[$line]) && ($coverage[$line] !== null)) { $methodCount = max($methodCount, count($coverage[$line])); } } $lines[$method['startLine']] = [ 'ccn' => $method['ccn'], 'count' => $methodCount, 'crap' => $method['crap'], 'type' => 'method', 'visibility' => $method['visibility'], 'name' => $methodName ]; } if (!empty($class['package']['namespace'])) { $namespace = $class['package']['namespace']; } $xmlClass = $xmlDocument->createElement('class'); $xmlClass->setAttribute('name', $className); $xmlClass->setAttribute('namespace', $namespace); if (!empty($class['package']['fullPackage'])) { $xmlClass->setAttribute( 'fullPackage', $class['package']['fullPackage'] ); } if (!empty($class['package']['category'])) { $xmlClass->setAttribute( 'category', $class['package']['category'] ); } if (!empty($class['package']['package'])) { $xmlClass->setAttribute( 'package', $class['package']['package'] ); } if (!empty($class['package']['subpackage'])) { $xmlClass->setAttribute( 'subpackage', $class['package']['subpackage'] ); } $xmlFile->appendChild($xmlClass); $xmlMetrics = $xmlDocument->createElement('metrics'); $xmlMetrics->setAttribute('complexity', $class['ccn']); $xmlMetrics->setAttribute('methods', $classMethods); $xmlMetrics->setAttribute('coveredmethods', $coveredMethods); $xmlMetrics->setAttribute('conditionals', 0); $xmlMetrics->setAttribute('coveredconditionals', 0); $xmlMetrics->setAttribute('statements', $classStatements); $xmlMetrics->setAttribute('coveredstatements', $coveredClassStatements); $xmlMetrics->setAttribute('elements', $classMethods + $classStatements ); $xmlMetrics->setAttribute('coveredelements', $coveredMethods + $coveredClassStatements ); $xmlClass->appendChild($xmlMetrics); } foreach ($coverage as $line => $data) { if ($data === null || isset($lines[$line])) { continue; } $lines[$line] = [ 'count' => count($data), 'type' => 'stmt' ]; } ksort($lines); foreach ($lines as $line => $data) { $xmlLine = $xmlDocument->createElement('line'); $xmlLine->setAttribute('num', $line); $xmlLine->setAttribute('type', $data['type']); if (isset($data['name'])) { $xmlLine->setAttribute('name', $data['name']); } if (isset($data['visibility'])) { $xmlLine->setAttribute('visibility', $data['visibility']); } if (isset($data['ccn'])) { $xmlLine->setAttribute('complexity', $data['ccn']); } if (isset($data['crap'])) { $xmlLine->setAttribute('crap', $data['crap']); } $xmlLine->setAttribute('count', $data['count']); $xmlFile->appendChild($xmlLine); } $linesOfCode = $item->getLinesOfCode(); $xmlMetrics = $xmlDocument->createElement('metrics'); $xmlMetrics->setAttribute('loc', $linesOfCode['loc']); $xmlMetrics->setAttribute('ncloc', $linesOfCode['ncloc']); $xmlMetrics->setAttribute('classes', $item->getNumClassesAndTraits()); $xmlMetrics->setAttribute('methods', $item->getNumMethods()); $xmlMetrics->setAttribute('coveredmethods', $item->getNumTestedMethods()); $xmlMetrics->setAttribute('conditionals', 0); $xmlMetrics->setAttribute('coveredconditionals', 0); $xmlMetrics->setAttribute('statements', $item->getNumExecutableLines()); $xmlMetrics->setAttribute('coveredstatements', $item->getNumExecutedLines()); $xmlMetrics->setAttribute('elements', $item->getNumMethods() + $item->getNumExecutableLines() ); $xmlMetrics->setAttribute('coveredelements', $item->getNumTestedMethods() + $item->getNumExecutedLines() ); $xmlFile->appendChild($xmlMetrics); if ($namespace == 'global') { $xmlProject->appendChild($xmlFile); } else { if (!isset($packages[$namespace])) { $packages[$namespace] = $xmlDocument->createElement( 'package' ); $packages[$namespace]->setAttribute('name', $namespace); $xmlProject->appendChild($packages[$namespace]); } $packages[$namespace]->appendChild($xmlFile); } } $linesOfCode = $report->getLinesOfCode(); $xmlMetrics = $xmlDocument->createElement('metrics'); $xmlMetrics->setAttribute('files', count($report)); $xmlMetrics->setAttribute('loc', $linesOfCode['loc']); $xmlMetrics->setAttribute('ncloc', $linesOfCode['ncloc']); $xmlMetrics->setAttribute('classes', $report->getNumClassesAndTraits()); $xmlMetrics->setAttribute('methods', $report->getNumMethods()); $xmlMetrics->setAttribute('coveredmethods', $report->getNumTestedMethods()); $xmlMetrics->setAttribute('conditionals', 0); $xmlMetrics->setAttribute('coveredconditionals', 0); $xmlMetrics->setAttribute('statements', $report->getNumExecutableLines()); $xmlMetrics->setAttribute('coveredstatements', $report->getNumExecutedLines()); $xmlMetrics->setAttribute('elements', $report->getNumMethods() + $report->getNumExecutableLines() ); $xmlMetrics->setAttribute('coveredelements', $report->getNumTestedMethods() + $report->getNumExecutedLines() ); $xmlProject->appendChild($xmlMetrics); $buffer = $xmlDocument->saveXML(); if ($target !== null) { if (!is_dir(dirname($target))) { mkdir(dirname($target), 0777, true); } file_put_contents($target, $buffer); } return $buffer; } } threshold = $threshold; } public function process(CodeCoverage $coverage, $target = null, $name = null) { $document = new \DOMDocument('1.0', 'UTF-8'); $document->formatOutput = true; $root = $document->createElement('crap_result'); $document->appendChild($root); $project = $document->createElement('project', is_string($name) ? $name : ''); $root->appendChild($project); $root->appendChild($document->createElement('timestamp', date('Y-m-d H:i:s', (int) $_SERVER['REQUEST_TIME']))); $stats = $document->createElement('stats'); $methodsNode = $document->createElement('methods'); $report = $coverage->getReport(); unset($coverage); $fullMethodCount = 0; $fullCrapMethodCount = 0; $fullCrapLoad = 0; $fullCrap = 0; foreach ($report as $item) { $namespace = 'global'; if (!$item instanceof File) { continue; } $file = $document->createElement('file'); $file->setAttribute('name', $item->getPath()); $classes = $item->getClassesAndTraits(); foreach ($classes as $className => $class) { foreach ($class['methods'] as $methodName => $method) { $crapLoad = $this->getCrapLoad($method['crap'], $method['ccn'], $method['coverage']); $fullCrap += $method['crap']; $fullCrapLoad += $crapLoad; $fullMethodCount++; if ($method['crap'] >= $this->threshold) { $fullCrapMethodCount++; } $methodNode = $document->createElement('method'); if (!empty($class['package']['namespace'])) { $namespace = $class['package']['namespace']; } $methodNode->appendChild($document->createElement('package', $namespace)); $methodNode->appendChild($document->createElement('className', $className)); $methodNode->appendChild($document->createElement('methodName', $methodName)); $methodNode->appendChild($document->createElement('methodSignature', htmlspecialchars($method['signature']))); $methodNode->appendChild($document->createElement('fullMethod', htmlspecialchars($method['signature']))); $methodNode->appendChild($document->createElement('crap', $this->roundValue($method['crap']))); $methodNode->appendChild($document->createElement('complexity', $method['ccn'])); $methodNode->appendChild($document->createElement('coverage', $this->roundValue($method['coverage']))); $methodNode->appendChild($document->createElement('crapLoad', round($crapLoad))); $methodsNode->appendChild($methodNode); } } } $stats->appendChild($document->createElement('name', 'Method Crap Stats')); $stats->appendChild($document->createElement('methodCount', $fullMethodCount)); $stats->appendChild($document->createElement('crapMethodCount', $fullCrapMethodCount)); $stats->appendChild($document->createElement('crapLoad', round($fullCrapLoad))); $stats->appendChild($document->createElement('totalCrap', $fullCrap)); if ($fullMethodCount > 0) { $crapMethodPercent = $this->roundValue((100 * $fullCrapMethodCount) / $fullMethodCount); } else { $crapMethodPercent = 0; } $stats->appendChild($document->createElement('crapMethodPercent', $crapMethodPercent)); $root->appendChild($stats); $root->appendChild($methodsNode); $buffer = $document->saveXML(); if ($target !== null) { if (!is_dir(dirname($target))) { mkdir(dirname($target), 0777, true); } file_put_contents($target, $buffer); } return $buffer; } private function getCrapLoad($crapValue, $cyclomaticComplexity, $coveragePercent) { $crapLoad = 0; if ($crapValue >= $this->threshold) { $crapLoad += $cyclomaticComplexity * (1.0 - $coveragePercent / 100); $crapLoad += $cyclomaticComplexity / $this->threshold; } return $crapLoad; } private function roundValue($value) { return round($value, 2); } } generator = $generator; $this->highLowerBound = $highLowerBound; $this->lowUpperBound = $lowUpperBound; $this->templatePath = __DIR__ . '/Renderer/Template/'; } public function process(CodeCoverage $coverage, $target) { $target = $this->getDirectory($target); $report = $coverage->getReport(); unset($coverage); if (!isset($_SERVER['REQUEST_TIME'])) { $_SERVER['REQUEST_TIME'] = time(); } $date = date('D M j G:i:s T Y', $_SERVER['REQUEST_TIME']); $dashboard = new Dashboard( $this->templatePath, $this->generator, $date, $this->lowUpperBound, $this->highLowerBound ); $directory = new Directory( $this->templatePath, $this->generator, $date, $this->lowUpperBound, $this->highLowerBound ); $file = new File( $this->templatePath, $this->generator, $date, $this->lowUpperBound, $this->highLowerBound ); $directory->render($report, $target . 'index.html'); $dashboard->render($report, $target . 'dashboard.html'); foreach ($report as $node) { $id = $node->getId(); if ($node instanceof DirectoryNode) { if (!file_exists($target . $id)) { mkdir($target . $id, 0777, true); } $directory->render($node, $target . $id . '/index.html'); $dashboard->render($node, $target . $id . '/dashboard.html'); } else { $dir = dirname($target . $id); if (!file_exists($dir)) { mkdir($dir, 0777, true); } $file->render($node, $target . $id . '.html'); } } $this->copyFiles($target); } private function copyFiles($target) { $dir = $this->getDirectory($target . 'css'); copy($this->templatePath . 'css/bootstrap.min.css', $dir . 'bootstrap.min.css'); copy($this->templatePath . 'css/nv.d3.min.css', $dir . 'nv.d3.min.css'); copy($this->templatePath . 'css/style.css', $dir . 'style.css'); $dir = $this->getDirectory($target . 'fonts'); copy($this->templatePath . 'fonts/glyphicons-halflings-regular.eot', $dir . 'glyphicons-halflings-regular.eot'); copy($this->templatePath . 'fonts/glyphicons-halflings-regular.svg', $dir . 'glyphicons-halflings-regular.svg'); copy($this->templatePath . 'fonts/glyphicons-halflings-regular.ttf', $dir . 'glyphicons-halflings-regular.ttf'); copy($this->templatePath . 'fonts/glyphicons-halflings-regular.woff', $dir . 'glyphicons-halflings-regular.woff'); copy($this->templatePath . 'fonts/glyphicons-halflings-regular.woff2', $dir . 'glyphicons-halflings-regular.woff2'); $dir = $this->getDirectory($target . 'js'); copy($this->templatePath . 'js/bootstrap.min.js', $dir . 'bootstrap.min.js'); copy($this->templatePath . 'js/d3.min.js', $dir . 'd3.min.js'); copy($this->templatePath . 'js/holder.min.js', $dir . 'holder.min.js'); copy($this->templatePath . 'js/html5shiv.min.js', $dir . 'html5shiv.min.js'); copy($this->templatePath . 'js/jquery.min.js', $dir . 'jquery.min.js'); copy($this->templatePath . 'js/nv.d3.min.js', $dir . 'nv.d3.min.js'); copy($this->templatePath . 'js/respond.min.js', $dir . 'respond.min.js'); } private function getDirectory($directory) { if (substr($directory, -1, 1) != DIRECTORY_SEPARATOR) { $directory .= DIRECTORY_SEPARATOR; } if (is_dir($directory)) { return $directory; } if (@mkdir($directory, 0777, true)) { return $directory; } throw new RuntimeException( sprintf( 'Directory "%s" does not exist.', $directory ) ); } } getClassesAndTraits(); $template = new \Text_Template( $this->templatePath . 'dashboard.html', '{{', '}}' ); $this->setCommonTemplateVariables($template, $node); $baseLink = $node->getId() . '/'; $complexity = $this->complexity($classes, $baseLink); $coverageDistribution = $this->coverageDistribution($classes); $insufficientCoverage = $this->insufficientCoverage($classes, $baseLink); $projectRisks = $this->projectRisks($classes, $baseLink); $template->setVar( [ 'insufficient_coverage_classes' => $insufficientCoverage['class'], 'insufficient_coverage_methods' => $insufficientCoverage['method'], 'project_risks_classes' => $projectRisks['class'], 'project_risks_methods' => $projectRisks['method'], 'complexity_class' => $complexity['class'], 'complexity_method' => $complexity['method'], 'class_coverage_distribution' => $coverageDistribution['class'], 'method_coverage_distribution' => $coverageDistribution['method'] ] ); $template->renderTo($file); } protected function complexity(array $classes, $baseLink) { $result = ['class' => [], 'method' => []]; foreach ($classes as $className => $class) { foreach ($class['methods'] as $methodName => $method) { if ($className != '*') { $methodName = $className . '::' . $methodName; } $result['method'][] = [ $method['coverage'], $method['ccn'], sprintf( '%s', str_replace($baseLink, '', $method['link']), $methodName ) ]; } $result['class'][] = [ $class['coverage'], $class['ccn'], sprintf( '%s', str_replace($baseLink, '', $class['link']), $className ) ]; } return [ 'class' => json_encode($result['class']), 'method' => json_encode($result['method']) ]; } protected function coverageDistribution(array $classes) { $result = [ 'class' => [ '0%' => 0, '0-10%' => 0, '10-20%' => 0, '20-30%' => 0, '30-40%' => 0, '40-50%' => 0, '50-60%' => 0, '60-70%' => 0, '70-80%' => 0, '80-90%' => 0, '90-100%' => 0, '100%' => 0 ], 'method' => [ '0%' => 0, '0-10%' => 0, '10-20%' => 0, '20-30%' => 0, '30-40%' => 0, '40-50%' => 0, '50-60%' => 0, '60-70%' => 0, '70-80%' => 0, '80-90%' => 0, '90-100%' => 0, '100%' => 0 ] ]; foreach ($classes as $class) { foreach ($class['methods'] as $methodName => $method) { if ($method['coverage'] == 0) { $result['method']['0%']++; } elseif ($method['coverage'] == 100) { $result['method']['100%']++; } else { $key = floor($method['coverage'] / 10) * 10; $key = $key . '-' . ($key + 10) . '%'; $result['method'][$key]++; } } if ($class['coverage'] == 0) { $result['class']['0%']++; } elseif ($class['coverage'] == 100) { $result['class']['100%']++; } else { $key = floor($class['coverage'] / 10) * 10; $key = $key . '-' . ($key + 10) . '%'; $result['class'][$key]++; } } return [ 'class' => json_encode(array_values($result['class'])), 'method' => json_encode(array_values($result['method'])) ]; } protected function insufficientCoverage(array $classes, $baseLink) { $leastTestedClasses = []; $leastTestedMethods = []; $result = ['class' => '', 'method' => '']; foreach ($classes as $className => $class) { foreach ($class['methods'] as $methodName => $method) { if ($method['coverage'] < $this->highLowerBound) { if ($className != '*') { $key = $className . '::' . $methodName; } else { $key = $methodName; } $leastTestedMethods[$key] = $method['coverage']; } } if ($class['coverage'] < $this->highLowerBound) { $leastTestedClasses[$className] = $class['coverage']; } } asort($leastTestedClasses); asort($leastTestedMethods); foreach ($leastTestedClasses as $className => $coverage) { $result['class'] .= sprintf( ' %s%d%%' . "\n", str_replace($baseLink, '', $classes[$className]['link']), $className, $coverage ); } foreach ($leastTestedMethods as $methodName => $coverage) { list($class, $method) = explode('::', $methodName); $result['method'] .= sprintf( ' %s%d%%' . "\n", str_replace($baseLink, '', $classes[$class]['methods'][$method]['link']), $methodName, $method, $coverage ); } return $result; } protected function projectRisks(array $classes, $baseLink) { $classRisks = []; $methodRisks = []; $result = ['class' => '', 'method' => '']; foreach ($classes as $className => $class) { foreach ($class['methods'] as $methodName => $method) { if ($method['coverage'] < $this->highLowerBound && $method['ccn'] > 1) { if ($className != '*') { $key = $className . '::' . $methodName; } else { $key = $methodName; } $methodRisks[$key] = $method['crap']; } } if ($class['coverage'] < $this->highLowerBound && $class['ccn'] > count($class['methods'])) { $classRisks[$className] = $class['crap']; } } arsort($classRisks); arsort($methodRisks); foreach ($classRisks as $className => $crap) { $result['class'] .= sprintf( ' %s%d' . "\n", str_replace($baseLink, '', $classes[$className]['link']), $className, $crap ); } foreach ($methodRisks as $methodName => $crap) { list($class, $method) = explode('::', $methodName); $result['method'] .= sprintf( ' %s%d' . "\n", str_replace($baseLink, '', $classes[$class]['methods'][$method]['link']), $methodName, $method, $crap ); } return $result; } protected function getActiveBreadcrumb(AbstractNode $node) { return sprintf( '
  • %s
  • ' . "\n" . '
  • (Dashboard)
  • ' . "\n", $node->getName() ); } } templatePath . 'directory.html', '{{', '}}'); $this->setCommonTemplateVariables($template, $node); $items = $this->renderItem($node, true); foreach ($node->getDirectories() as $item) { $items .= $this->renderItem($item); } foreach ($node->getFiles() as $item) { $items .= $this->renderItem($item); } $template->setVar( [ 'id' => $node->getId(), 'items' => $items ] ); $template->renderTo($file); } protected function renderItem(Node $node, $total = false) { $data = [ 'numClasses' => $node->getNumClassesAndTraits(), 'numTestedClasses' => $node->getNumTestedClassesAndTraits(), 'numMethods' => $node->getNumMethods(), 'numTestedMethods' => $node->getNumTestedMethods(), 'linesExecutedPercent' => $node->getLineExecutedPercent(false), 'linesExecutedPercentAsString' => $node->getLineExecutedPercent(), 'numExecutedLines' => $node->getNumExecutedLines(), 'numExecutableLines' => $node->getNumExecutableLines(), 'testedMethodsPercent' => $node->getTestedMethodsPercent(false), 'testedMethodsPercentAsString' => $node->getTestedMethodsPercent(), 'testedClassesPercent' => $node->getTestedClassesAndTraitsPercent(false), 'testedClassesPercentAsString' => $node->getTestedClassesAndTraitsPercent() ]; if ($total) { $data['name'] = 'Total'; } else { if ($node instanceof DirectoryNode) { $data['name'] = sprintf( '%s', $node->getName(), $node->getName() ); $data['icon'] = ' '; } else { $data['name'] = sprintf( '%s', $node->getName(), $node->getName() ); $data['icon'] = ' '; } } return $this->renderItemTemplate( new \Text_Template($this->templatePath . 'directory_item.html', '{{', '}}'), $data ); } } htmlspecialcharsFlags = ENT_COMPAT; $this->htmlspecialcharsFlags = $this->htmlspecialcharsFlags | ENT_HTML401 | ENT_SUBSTITUTE; } public function render(FileNode $node, $file) { $template = new \Text_Template($this->templatePath . 'file.html', '{{', '}}'); $template->setVar( [ 'items' => $this->renderItems($node), 'lines' => $this->renderSource($node) ] ); $this->setCommonTemplateVariables($template, $node); $template->renderTo($file); } protected function renderItems(FileNode $node) { $template = new \Text_Template($this->templatePath . 'file_item.html', '{{', '}}'); $methodItemTemplate = new \Text_Template( $this->templatePath . 'method_item.html', '{{', '}}' ); $items = $this->renderItemTemplate( $template, [ 'name' => 'Total', 'numClasses' => $node->getNumClassesAndTraits(), 'numTestedClasses' => $node->getNumTestedClassesAndTraits(), 'numMethods' => $node->getNumMethods(), 'numTestedMethods' => $node->getNumTestedMethods(), 'linesExecutedPercent' => $node->getLineExecutedPercent(false), 'linesExecutedPercentAsString' => $node->getLineExecutedPercent(), 'numExecutedLines' => $node->getNumExecutedLines(), 'numExecutableLines' => $node->getNumExecutableLines(), 'testedMethodsPercent' => $node->getTestedMethodsPercent(false), 'testedMethodsPercentAsString' => $node->getTestedMethodsPercent(), 'testedClassesPercent' => $node->getTestedClassesAndTraitsPercent(false), 'testedClassesPercentAsString' => $node->getTestedClassesAndTraitsPercent(), 'crap' => 'CRAP' ] ); $items .= $this->renderFunctionItems( $node->getFunctions(), $methodItemTemplate ); $items .= $this->renderTraitOrClassItems( $node->getTraits(), $template, $methodItemTemplate ); $items .= $this->renderTraitOrClassItems( $node->getClasses(), $template, $methodItemTemplate ); return $items; } protected function renderTraitOrClassItems(array $items, \Text_Template $template, \Text_Template $methodItemTemplate) { if (empty($items)) { return ''; } $buffer = ''; foreach ($items as $name => $item) { $numMethods = count($item['methods']); $numTestedMethods = 0; foreach ($item['methods'] as $method) { if ($method['executedLines'] == $method['executableLines']) { $numTestedMethods++; } } if ($item['executableLines'] > 0) { $numClasses = 1; $numTestedClasses = $numTestedMethods == $numMethods ? 1 : 0; $linesExecutedPercentAsString = Util::percent( $item['executedLines'], $item['executableLines'], true ); } else { $numClasses = 'n/a'; $numTestedClasses = 'n/a'; $linesExecutedPercentAsString = 'n/a'; } $buffer .= $this->renderItemTemplate( $template, [ 'name' => $name, 'numClasses' => $numClasses, 'numTestedClasses' => $numTestedClasses, 'numMethods' => $numMethods, 'numTestedMethods' => $numTestedMethods, 'linesExecutedPercent' => Util::percent( $item['executedLines'], $item['executableLines'], false ), 'linesExecutedPercentAsString' => $linesExecutedPercentAsString, 'numExecutedLines' => $item['executedLines'], 'numExecutableLines' => $item['executableLines'], 'testedMethodsPercent' => Util::percent( $numTestedMethods, $numMethods, false ), 'testedMethodsPercentAsString' => Util::percent( $numTestedMethods, $numMethods, true ), 'testedClassesPercent' => Util::percent( $numTestedMethods == $numMethods ? 1 : 0, 1, false ), 'testedClassesPercentAsString' => Util::percent( $numTestedMethods == $numMethods ? 1 : 0, 1, true ), 'crap' => $item['crap'] ] ); foreach ($item['methods'] as $method) { $buffer .= $this->renderFunctionOrMethodItem( $methodItemTemplate, $method, ' ' ); } } return $buffer; } protected function renderFunctionItems(array $functions, \Text_Template $template) { if (empty($functions)) { return ''; } $buffer = ''; foreach ($functions as $function) { $buffer .= $this->renderFunctionOrMethodItem( $template, $function ); } return $buffer; } protected function renderFunctionOrMethodItem(\Text_Template $template, array $item, $indent = '') { $numTestedItems = $item['executedLines'] == $item['executableLines'] ? 1 : 0; return $this->renderItemTemplate( $template, [ 'name' => sprintf( '%s%s', $indent, $item['startLine'], htmlspecialchars($item['signature']), isset($item['functionName']) ? $item['functionName'] : $item['methodName'] ), 'numMethods' => 1, 'numTestedMethods' => $numTestedItems, 'linesExecutedPercent' => Util::percent( $item['executedLines'], $item['executableLines'], false ), 'linesExecutedPercentAsString' => Util::percent( $item['executedLines'], $item['executableLines'], true ), 'numExecutedLines' => $item['executedLines'], 'numExecutableLines' => $item['executableLines'], 'testedMethodsPercent' => Util::percent( $numTestedItems, 1, false ), 'testedMethodsPercentAsString' => Util::percent( $numTestedItems, 1, true ), 'crap' => $item['crap'] ] ); } protected function renderSource(FileNode $node) { $coverageData = $node->getCoverageData(); $testData = $node->getTestData(); $codeLines = $this->loadFile($node->getPath()); $lines = ''; $i = 1; foreach ($codeLines as $line) { $trClass = ''; $popoverContent = ''; $popoverTitle = ''; if (array_key_exists($i, $coverageData)) { $numTests = count($coverageData[$i]); if ($coverageData[$i] === null) { $trClass = ' class="warning"'; } elseif ($numTests == 0) { $trClass = ' class="danger"'; } else { $lineCss = 'covered-by-large-tests'; $popoverContent = '
      '; if ($numTests > 1) { $popoverTitle = $numTests . ' tests cover line ' . $i; } else { $popoverTitle = '1 test covers line ' . $i; } foreach ($coverageData[$i] as $test) { if ($lineCss == 'covered-by-large-tests' && $testData[$test]['size'] == 'medium') { $lineCss = 'covered-by-medium-tests'; } elseif ($testData[$test]['size'] == 'small') { $lineCss = 'covered-by-small-tests'; } switch ($testData[$test]['status']) { case 0: switch ($testData[$test]['size']) { case 'small': $testCSS = ' class="covered-by-small-tests"'; break; case 'medium': $testCSS = ' class="covered-by-medium-tests"'; break; default: $testCSS = ' class="covered-by-large-tests"'; break; } break; case 1: case 2: $testCSS = ' class="warning"'; break; case 3: $testCSS = ' class="danger"'; break; case 4: $testCSS = ' class="danger"'; break; default: $testCSS = ''; } $popoverContent .= sprintf( '%s', $testCSS, htmlspecialchars($test) ); } $popoverContent .= '
    '; $trClass = ' class="' . $lineCss . ' popin"'; } } if (!empty($popoverTitle)) { $popover = sprintf( ' data-title="%s" data-content="%s" data-placement="bottom" data-html="true"', $popoverTitle, htmlspecialchars($popoverContent) ); } else { $popover = ''; } $lines .= sprintf( ' %s' . "\n", $trClass, $popover, $i, $i, $i, $line ); $i++; } return $lines; } protected function loadFile($file) { $buffer = file_get_contents($file); $tokens = token_get_all($buffer); $result = ['']; $i = 0; $stringFlag = false; $fileEndsWithNewLine = substr($buffer, -1) == "\n"; unset($buffer); foreach ($tokens as $j => $token) { if (is_string($token)) { if ($token === '"' && $tokens[$j - 1] !== '\\') { $result[$i] .= sprintf( '%s', htmlspecialchars($token) ); $stringFlag = !$stringFlag; } else { $result[$i] .= sprintf( '%s', htmlspecialchars($token) ); } continue; } list($token, $value) = $token; $value = str_replace( ["\t", ' '], ['    ', ' '], htmlspecialchars($value, $this->htmlspecialcharsFlags) ); if ($value === "\n") { $result[++$i] = ''; } else { $lines = explode("\n", $value); foreach ($lines as $jj => $line) { $line = trim($line); if ($line !== '') { if ($stringFlag) { $colour = 'string'; } else { switch ($token) { case T_INLINE_HTML: $colour = 'html'; break; case T_COMMENT: case T_DOC_COMMENT: $colour = 'comment'; break; case T_ABSTRACT: case T_ARRAY: case T_AS: case T_BREAK: case T_CALLABLE: case T_CASE: case T_CATCH: case T_CLASS: case T_CLONE: case T_CONTINUE: case T_DEFAULT: case T_ECHO: case T_ELSE: case T_ELSEIF: case T_EMPTY: case T_ENDDECLARE: case T_ENDFOR: case T_ENDFOREACH: case T_ENDIF: case T_ENDSWITCH: case T_ENDWHILE: case T_EXIT: case T_EXTENDS: case T_FINAL: case T_FINALLY: case T_FOREACH: case T_FUNCTION: case T_GLOBAL: case T_IF: case T_IMPLEMENTS: case T_INCLUDE: case T_INCLUDE_ONCE: case T_INSTANCEOF: case T_INSTEADOF: case T_INTERFACE: case T_ISSET: case T_LOGICAL_AND: case T_LOGICAL_OR: case T_LOGICAL_XOR: case T_NAMESPACE: case T_NEW: case T_PRIVATE: case T_PROTECTED: case T_PUBLIC: case T_REQUIRE: case T_REQUIRE_ONCE: case T_RETURN: case T_STATIC: case T_THROW: case T_TRAIT: case T_TRY: case T_UNSET: case T_USE: case T_VAR: case T_WHILE: case T_YIELD: $colour = 'keyword'; break; default: $colour = 'default'; } } $result[$i] .= sprintf( '%s', $colour, $line ); } if (isset($lines[$jj + 1])) { $result[++$i] = ''; } } } } if ($fileEndsWithNewLine) { unset($result[count($result)-1]); } return $result; } }
    {{percent}}% covered ({{level}})
    /*! * Bootstrap v3.3.7 (http://getbootstrap.com) * Copyright 2011-2016 Twitter, Inc. * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) *//*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */html{font-family:sans-serif;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background-color:transparent}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:700}dfn{font-style:italic}h1{margin:.67em 0;font-size:2em}mark{color:#000;background:#ff0}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{height:0;-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}button,input,optgroup,select,textarea{margin:0;font:inherit;color:inherit}button{overflow:visible}button,select{text-transform:none}button,html input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{padding:0;border:0}input{line-height:normal}input[type=checkbox],input[type=radio]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;padding:0}input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{height:auto}input[type=search]{-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;-webkit-appearance:textfield}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}fieldset{padding:.35em .625em .75em;margin:0 2px;border:1px solid silver}legend{padding:0;border:0}textarea{overflow:auto}optgroup{font-weight:700}table{border-spacing:0;border-collapse:collapse}td,th{padding:0}/*! Source: https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css */@media print{*,:after,:before{color:#000!important;text-shadow:none!important;background:0 0!important;-webkit-box-shadow:none!important;box-shadow:none!important}a,a:visited{text-decoration:underline}a[href]:after{content:" (" attr(href) ")"}abbr[title]:after{content:" (" attr(title) ")"}a[href^="javascript:"]:after,a[href^="#"]:after{content:""}blockquote,pre{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}img,tr{page-break-inside:avoid}img{max-width:100%!important}h2,h3,p{orphans:3;widows:3}h2,h3{page-break-after:avoid}.navbar{display:none}.btn>.caret,.dropup>.btn>.caret{border-top-color:#000!important}.label{border:1px solid #000}.table{border-collapse:collapse!important}.table td,.table th{background-color:#fff!important}.table-bordered td,.table-bordered th{border:1px solid #ddd!important}}@font-face{font-family:'Glyphicons Halflings';src:url(../fonts/glyphicons-halflings-regular.eot);src:url(../fonts/glyphicons-halflings-regular.eot?#iefix) format('embedded-opentype'),url(../fonts/glyphicons-halflings-regular.woff2) format('woff2'),url(../fonts/glyphicons-halflings-regular.woff) format('woff'),url(../fonts/glyphicons-halflings-regular.ttf) format('truetype'),url(../fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular) format('svg')}.glyphicon{position:relative;top:1px;display:inline-block;font-family:'Glyphicons Halflings';font-style:normal;font-weight:400;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.glyphicon-asterisk:before{content:"\002a"}.glyphicon-plus:before{content:"\002b"}.glyphicon-eur:before,.glyphicon-euro:before{content:"\20ac"}.glyphicon-minus:before{content:"\2212"}.glyphicon-cloud:before{content:"\2601"}.glyphicon-envelope:before{content:"\2709"}.glyphicon-pencil:before{content:"\270f"}.glyphicon-glass:before{content:"\e001"}.glyphicon-music:before{content:"\e002"}.glyphicon-search:before{content:"\e003"}.glyphicon-heart:before{content:"\e005"}.glyphicon-star:before{content:"\e006"}.glyphicon-star-empty:before{content:"\e007"}.glyphicon-user:before{content:"\e008"}.glyphicon-film:before{content:"\e009"}.glyphicon-th-large:before{content:"\e010"}.glyphicon-th:before{content:"\e011"}.glyphicon-th-list:before{content:"\e012"}.glyphicon-ok:before{content:"\e013"}.glyphicon-remove:before{content:"\e014"}.glyphicon-zoom-in:before{content:"\e015"}.glyphicon-zoom-out:before{content:"\e016"}.glyphicon-off:before{content:"\e017"}.glyphicon-signal:before{content:"\e018"}.glyphicon-cog:before{content:"\e019"}.glyphicon-trash:before{content:"\e020"}.glyphicon-home:before{content:"\e021"}.glyphicon-file:before{content:"\e022"}.glyphicon-time:before{content:"\e023"}.glyphicon-road:before{content:"\e024"}.glyphicon-download-alt:before{content:"\e025"}.glyphicon-download:before{content:"\e026"}.glyphicon-upload:before{content:"\e027"}.glyphicon-inbox:before{content:"\e028"}.glyphicon-play-circle:before{content:"\e029"}.glyphicon-repeat:before{content:"\e030"}.glyphicon-refresh:before{content:"\e031"}.glyphicon-list-alt:before{content:"\e032"}.glyphicon-lock:before{content:"\e033"}.glyphicon-flag:before{content:"\e034"}.glyphicon-headphones:before{content:"\e035"}.glyphicon-volume-off:before{content:"\e036"}.glyphicon-volume-down:before{content:"\e037"}.glyphicon-volume-up:before{content:"\e038"}.glyphicon-qrcode:before{content:"\e039"}.glyphicon-barcode:before{content:"\e040"}.glyphicon-tag:before{content:"\e041"}.glyphicon-tags:before{content:"\e042"}.glyphicon-book:before{content:"\e043"}.glyphicon-bookmark:before{content:"\e044"}.glyphicon-print:before{content:"\e045"}.glyphicon-camera:before{content:"\e046"}.glyphicon-font:before{content:"\e047"}.glyphicon-bold:before{content:"\e048"}.glyphicon-italic:before{content:"\e049"}.glyphicon-text-height:before{content:"\e050"}.glyphicon-text-width:before{content:"\e051"}.glyphicon-align-left:before{content:"\e052"}.glyphicon-align-center:before{content:"\e053"}.glyphicon-align-right:before{content:"\e054"}.glyphicon-align-justify:before{content:"\e055"}.glyphicon-list:before{content:"\e056"}.glyphicon-indent-left:before{content:"\e057"}.glyphicon-indent-right:before{content:"\e058"}.glyphicon-facetime-video:before{content:"\e059"}.glyphicon-picture:before{content:"\e060"}.glyphicon-map-marker:before{content:"\e062"}.glyphicon-adjust:before{content:"\e063"}.glyphicon-tint:before{content:"\e064"}.glyphicon-edit:before{content:"\e065"}.glyphicon-share:before{content:"\e066"}.glyphicon-check:before{content:"\e067"}.glyphicon-move:before{content:"\e068"}.glyphicon-step-backward:before{content:"\e069"}.glyphicon-fast-backward:before{content:"\e070"}.glyphicon-backward:before{content:"\e071"}.glyphicon-play:before{content:"\e072"}.glyphicon-pause:before{content:"\e073"}.glyphicon-stop:before{content:"\e074"}.glyphicon-forward:before{content:"\e075"}.glyphicon-fast-forward:before{content:"\e076"}.glyphicon-step-forward:before{content:"\e077"}.glyphicon-eject:before{content:"\e078"}.glyphicon-chevron-left:before{content:"\e079"}.glyphicon-chevron-right:before{content:"\e080"}.glyphicon-plus-sign:before{content:"\e081"}.glyphicon-minus-sign:before{content:"\e082"}.glyphicon-remove-sign:before{content:"\e083"}.glyphicon-ok-sign:before{content:"\e084"}.glyphicon-question-sign:before{content:"\e085"}.glyphicon-info-sign:before{content:"\e086"}.glyphicon-screenshot:before{content:"\e087"}.glyphicon-remove-circle:before{content:"\e088"}.glyphicon-ok-circle:before{content:"\e089"}.glyphicon-ban-circle:before{content:"\e090"}.glyphicon-arrow-left:before{content:"\e091"}.glyphicon-arrow-right:before{content:"\e092"}.glyphicon-arrow-up:before{content:"\e093"}.glyphicon-arrow-down:before{content:"\e094"}.glyphicon-share-alt:before{content:"\e095"}.glyphicon-resize-full:before{content:"\e096"}.glyphicon-resize-small:before{content:"\e097"}.glyphicon-exclamation-sign:before{content:"\e101"}.glyphicon-gift:before{content:"\e102"}.glyphicon-leaf:before{content:"\e103"}.glyphicon-fire:before{content:"\e104"}.glyphicon-eye-open:before{content:"\e105"}.glyphicon-eye-close:before{content:"\e106"}.glyphicon-warning-sign:before{content:"\e107"}.glyphicon-plane:before{content:"\e108"}.glyphicon-calendar:before{content:"\e109"}.glyphicon-random:before{content:"\e110"}.glyphicon-comment:before{content:"\e111"}.glyphicon-magnet:before{content:"\e112"}.glyphicon-chevron-up:before{content:"\e113"}.glyphicon-chevron-down:before{content:"\e114"}.glyphicon-retweet:before{content:"\e115"}.glyphicon-shopping-cart:before{content:"\e116"}.glyphicon-folder-close:before{content:"\e117"}.glyphicon-folder-open:before{content:"\e118"}.glyphicon-resize-vertical:before{content:"\e119"}.glyphicon-resize-horizontal:before{content:"\e120"}.glyphicon-hdd:before{content:"\e121"}.glyphicon-bullhorn:before{content:"\e122"}.glyphicon-bell:before{content:"\e123"}.glyphicon-certificate:before{content:"\e124"}.glyphicon-thumbs-up:before{content:"\e125"}.glyphicon-thumbs-down:before{content:"\e126"}.glyphicon-hand-right:before{content:"\e127"}.glyphicon-hand-left:before{content:"\e128"}.glyphicon-hand-up:before{content:"\e129"}.glyphicon-hand-down:before{content:"\e130"}.glyphicon-circle-arrow-right:before{content:"\e131"}.glyphicon-circle-arrow-left:before{content:"\e132"}.glyphicon-circle-arrow-up:before{content:"\e133"}.glyphicon-circle-arrow-down:before{content:"\e134"}.glyphicon-globe:before{content:"\e135"}.glyphicon-wrench:before{content:"\e136"}.glyphicon-tasks:before{content:"\e137"}.glyphicon-filter:before{content:"\e138"}.glyphicon-briefcase:before{content:"\e139"}.glyphicon-fullscreen:before{content:"\e140"}.glyphicon-dashboard:before{content:"\e141"}.glyphicon-paperclip:before{content:"\e142"}.glyphicon-heart-empty:before{content:"\e143"}.glyphicon-link:before{content:"\e144"}.glyphicon-phone:before{content:"\e145"}.glyphicon-pushpin:before{content:"\e146"}.glyphicon-usd:before{content:"\e148"}.glyphicon-gbp:before{content:"\e149"}.glyphicon-sort:before{content:"\e150"}.glyphicon-sort-by-alphabet:before{content:"\e151"}.glyphicon-sort-by-alphabet-alt:before{content:"\e152"}.glyphicon-sort-by-order:before{content:"\e153"}.glyphicon-sort-by-order-alt:before{content:"\e154"}.glyphicon-sort-by-attributes:before{content:"\e155"}.glyphicon-sort-by-attributes-alt:before{content:"\e156"}.glyphicon-unchecked:before{content:"\e157"}.glyphicon-expand:before{content:"\e158"}.glyphicon-collapse-down:before{content:"\e159"}.glyphicon-collapse-up:before{content:"\e160"}.glyphicon-log-in:before{content:"\e161"}.glyphicon-flash:before{content:"\e162"}.glyphicon-log-out:before{content:"\e163"}.glyphicon-new-window:before{content:"\e164"}.glyphicon-record:before{content:"\e165"}.glyphicon-save:before{content:"\e166"}.glyphicon-open:before{content:"\e167"}.glyphicon-saved:before{content:"\e168"}.glyphicon-import:before{content:"\e169"}.glyphicon-export:before{content:"\e170"}.glyphicon-send:before{content:"\e171"}.glyphicon-floppy-disk:before{content:"\e172"}.glyphicon-floppy-saved:before{content:"\e173"}.glyphicon-floppy-remove:before{content:"\e174"}.glyphicon-floppy-save:before{content:"\e175"}.glyphicon-floppy-open:before{content:"\e176"}.glyphicon-credit-card:before{content:"\e177"}.glyphicon-transfer:before{content:"\e178"}.glyphicon-cutlery:before{content:"\e179"}.glyphicon-header:before{content:"\e180"}.glyphicon-compressed:before{content:"\e181"}.glyphicon-earphone:before{content:"\e182"}.glyphicon-phone-alt:before{content:"\e183"}.glyphicon-tower:before{content:"\e184"}.glyphicon-stats:before{content:"\e185"}.glyphicon-sd-video:before{content:"\e186"}.glyphicon-hd-video:before{content:"\e187"}.glyphicon-subtitles:before{content:"\e188"}.glyphicon-sound-stereo:before{content:"\e189"}.glyphicon-sound-dolby:before{content:"\e190"}.glyphicon-sound-5-1:before{content:"\e191"}.glyphicon-sound-6-1:before{content:"\e192"}.glyphicon-sound-7-1:before{content:"\e193"}.glyphicon-copyright-mark:before{content:"\e194"}.glyphicon-registration-mark:before{content:"\e195"}.glyphicon-cloud-download:before{content:"\e197"}.glyphicon-cloud-upload:before{content:"\e198"}.glyphicon-tree-conifer:before{content:"\e199"}.glyphicon-tree-deciduous:before{content:"\e200"}.glyphicon-cd:before{content:"\e201"}.glyphicon-save-file:before{content:"\e202"}.glyphicon-open-file:before{content:"\e203"}.glyphicon-level-up:before{content:"\e204"}.glyphicon-copy:before{content:"\e205"}.glyphicon-paste:before{content:"\e206"}.glyphicon-alert:before{content:"\e209"}.glyphicon-equalizer:before{content:"\e210"}.glyphicon-king:before{content:"\e211"}.glyphicon-queen:before{content:"\e212"}.glyphicon-pawn:before{content:"\e213"}.glyphicon-bishop:before{content:"\e214"}.glyphicon-knight:before{content:"\e215"}.glyphicon-baby-formula:before{content:"\e216"}.glyphicon-tent:before{content:"\26fa"}.glyphicon-blackboard:before{content:"\e218"}.glyphicon-bed:before{content:"\e219"}.glyphicon-apple:before{content:"\f8ff"}.glyphicon-erase:before{content:"\e221"}.glyphicon-hourglass:before{content:"\231b"}.glyphicon-lamp:before{content:"\e223"}.glyphicon-duplicate:before{content:"\e224"}.glyphicon-piggy-bank:before{content:"\e225"}.glyphicon-scissors:before{content:"\e226"}.glyphicon-bitcoin:before{content:"\e227"}.glyphicon-btc:before{content:"\e227"}.glyphicon-xbt:before{content:"\e227"}.glyphicon-yen:before{content:"\00a5"}.glyphicon-jpy:before{content:"\00a5"}.glyphicon-ruble:before{content:"\20bd"}.glyphicon-rub:before{content:"\20bd"}.glyphicon-scale:before{content:"\e230"}.glyphicon-ice-lolly:before{content:"\e231"}.glyphicon-ice-lolly-tasted:before{content:"\e232"}.glyphicon-education:before{content:"\e233"}.glyphicon-option-horizontal:before{content:"\e234"}.glyphicon-option-vertical:before{content:"\e235"}.glyphicon-menu-hamburger:before{content:"\e236"}.glyphicon-modal-window:before{content:"\e237"}.glyphicon-oil:before{content:"\e238"}.glyphicon-grain:before{content:"\e239"}.glyphicon-sunglasses:before{content:"\e240"}.glyphicon-text-size:before{content:"\e241"}.glyphicon-text-color:before{content:"\e242"}.glyphicon-text-background:before{content:"\e243"}.glyphicon-object-align-top:before{content:"\e244"}.glyphicon-object-align-bottom:before{content:"\e245"}.glyphicon-object-align-horizontal:before{content:"\e246"}.glyphicon-object-align-left:before{content:"\e247"}.glyphicon-object-align-vertical:before{content:"\e248"}.glyphicon-object-align-right:before{content:"\e249"}.glyphicon-triangle-right:before{content:"\e250"}.glyphicon-triangle-left:before{content:"\e251"}.glyphicon-triangle-bottom:before{content:"\e252"}.glyphicon-triangle-top:before{content:"\e253"}.glyphicon-console:before{content:"\e254"}.glyphicon-superscript:before{content:"\e255"}.glyphicon-subscript:before{content:"\e256"}.glyphicon-menu-left:before{content:"\e257"}.glyphicon-menu-right:before{content:"\e258"}.glyphicon-menu-down:before{content:"\e259"}.glyphicon-menu-up:before{content:"\e260"}*{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}:after,:before{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{font-size:10px;-webkit-tap-highlight-color:rgba(0,0,0,0)}body{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;line-height:1.42857143;color:#333;background-color:#fff}button,input,select,textarea{font-family:inherit;font-size:inherit;line-height:inherit}a{color:#337ab7;text-decoration:none}a:focus,a:hover{color:#23527c;text-decoration:underline}a:focus{outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}figure{margin:0}img{vertical-align:middle}.carousel-inner>.item>a>img,.carousel-inner>.item>img,.img-responsive,.thumbnail a>img,.thumbnail>img{display:block;max-width:100%;height:auto}.img-rounded{border-radius:6px}.img-thumbnail{display:inline-block;max-width:100%;height:auto;padding:4px;line-height:1.42857143;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:all .2s ease-in-out;-o-transition:all .2s ease-in-out;transition:all .2s ease-in-out}.img-circle{border-radius:50%}hr{margin-top:20px;margin-bottom:20px;border:0;border-top:1px solid #eee}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto}[role=button]{cursor:pointer}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{font-family:inherit;font-weight:500;line-height:1.1;color:inherit}.h1 .small,.h1 small,.h2 .small,.h2 small,.h3 .small,.h3 small,.h4 .small,.h4 small,.h5 .small,.h5 small,.h6 .small,.h6 small,h1 .small,h1 small,h2 .small,h2 small,h3 .small,h3 small,h4 .small,h4 small,h5 .small,h5 small,h6 .small,h6 small{font-weight:400;line-height:1;color:#777}.h1,.h2,.h3,h1,h2,h3{margin-top:20px;margin-bottom:10px}.h1 .small,.h1 small,.h2 .small,.h2 small,.h3 .small,.h3 small,h1 .small,h1 small,h2 .small,h2 small,h3 .small,h3 small{font-size:65%}.h4,.h5,.h6,h4,h5,h6{margin-top:10px;margin-bottom:10px}.h4 .small,.h4 small,.h5 .small,.h5 small,.h6 .small,.h6 small,h4 .small,h4 small,h5 .small,h5 small,h6 .small,h6 small{font-size:75%}.h1,h1{font-size:36px}.h2,h2{font-size:30px}.h3,h3{font-size:24px}.h4,h4{font-size:18px}.h5,h5{font-size:14px}.h6,h6{font-size:12px}p{margin:0 0 10px}.lead{margin-bottom:20px;font-size:16px;font-weight:300;line-height:1.4}@media (min-width:768px){.lead{font-size:21px}}.small,small{font-size:85%}.mark,mark{padding:.2em;background-color:#fcf8e3}.text-left{text-align:left}.text-right{text-align:right}.text-center{text-align:center}.text-justify{text-align:justify}.text-nowrap{white-space:nowrap}.text-lowercase{text-transform:lowercase}.text-uppercase{text-transform:uppercase}.text-capitalize{text-transform:capitalize}.text-muted{color:#777}.text-primary{color:#337ab7}a.text-primary:focus,a.text-primary:hover{color:#286090}.text-success{color:#3c763d}a.text-success:focus,a.text-success:hover{color:#2b542c}.text-info{color:#31708f}a.text-info:focus,a.text-info:hover{color:#245269}.text-warning{color:#8a6d3b}a.text-warning:focus,a.text-warning:hover{color:#66512c}.text-danger{color:#a94442}a.text-danger:focus,a.text-danger:hover{color:#843534}.bg-primary{color:#fff;background-color:#337ab7}a.bg-primary:focus,a.bg-primary:hover{background-color:#286090}.bg-success{background-color:#dff0d8}a.bg-success:focus,a.bg-success:hover{background-color:#c1e2b3}.bg-info{background-color:#d9edf7}a.bg-info:focus,a.bg-info:hover{background-color:#afd9ee}.bg-warning{background-color:#fcf8e3}a.bg-warning:focus,a.bg-warning:hover{background-color:#f7ecb5}.bg-danger{background-color:#f2dede}a.bg-danger:focus,a.bg-danger:hover{background-color:#e4b9b9}.page-header{padding-bottom:9px;margin:40px 0 20px;border-bottom:1px solid #eee}ol,ul{margin-top:0;margin-bottom:10px}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;margin-left:-5px;list-style:none}.list-inline>li{display:inline-block;padding-right:5px;padding-left:5px}dl{margin-top:0;margin-bottom:20px}dd,dt{line-height:1.42857143}dt{font-weight:700}dd{margin-left:0}@media (min-width:768px){.dl-horizontal dt{float:left;width:160px;overflow:hidden;clear:left;text-align:right;text-overflow:ellipsis;white-space:nowrap}.dl-horizontal dd{margin-left:180px}}abbr[data-original-title],abbr[title]{cursor:help;border-bottom:1px dotted #777}.initialism{font-size:90%;text-transform:uppercase}blockquote{padding:10px 20px;margin:0 0 20px;font-size:17.5px;border-left:5px solid #eee}blockquote ol:last-child,blockquote p:last-child,blockquote ul:last-child{margin-bottom:0}blockquote .small,blockquote footer,blockquote small{display:block;font-size:80%;line-height:1.42857143;color:#777}blockquote .small:before,blockquote footer:before,blockquote small:before{content:'\2014 \00A0'}.blockquote-reverse,blockquote.pull-right{padding-right:15px;padding-left:0;text-align:right;border-right:5px solid #eee;border-left:0}.blockquote-reverse .small:before,.blockquote-reverse footer:before,.blockquote-reverse small:before,blockquote.pull-right .small:before,blockquote.pull-right footer:before,blockquote.pull-right small:before{content:''}.blockquote-reverse .small:after,.blockquote-reverse footer:after,.blockquote-reverse small:after,blockquote.pull-right .small:after,blockquote.pull-right footer:after,blockquote.pull-right small:after{content:'\00A0 \2014'}address{margin-bottom:20px;font-style:normal;line-height:1.42857143}code,kbd,pre,samp{font-family:Menlo,Monaco,Consolas,"Courier New",monospace}code{padding:2px 4px;font-size:90%;color:#c7254e;background-color:#f9f2f4;border-radius:4px}kbd{padding:2px 4px;font-size:90%;color:#fff;background-color:#333;border-radius:3px;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,.25);box-shadow:inset 0 -1px 0 rgba(0,0,0,.25)}kbd kbd{padding:0;font-size:100%;font-weight:700;-webkit-box-shadow:none;box-shadow:none}pre{display:block;padding:9.5px;margin:0 0 10px;font-size:13px;line-height:1.42857143;color:#333;word-break:break-all;word-wrap:break-word;background-color:#f5f5f5;border:1px solid #ccc;border-radius:4px}pre code{padding:0;font-size:inherit;color:inherit;white-space:pre-wrap;background-color:transparent;border-radius:0}.pre-scrollable{max-height:340px;overflow-y:scroll}.container{padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}@media (min-width:768px){.container{width:750px}}@media (min-width:992px){.container{width:970px}}@media (min-width:1200px){.container{width:1170px}}.container-fluid{padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}.row{margin-right:-15px;margin-left:-15px}.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-xs-1,.col-xs-10,.col-xs-11,.col-xs-12,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9{position:relative;min-height:1px;padding-right:15px;padding-left:15px}.col-xs-1,.col-xs-10,.col-xs-11,.col-xs-12,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9{float:left}.col-xs-12{width:100%}.col-xs-11{width:91.66666667%}.col-xs-10{width:83.33333333%}.col-xs-9{width:75%}.col-xs-8{width:66.66666667%}.col-xs-7{width:58.33333333%}.col-xs-6{width:50%}.col-xs-5{width:41.66666667%}.col-xs-4{width:33.33333333%}.col-xs-3{width:25%}.col-xs-2{width:16.66666667%}.col-xs-1{width:8.33333333%}.col-xs-pull-12{right:100%}.col-xs-pull-11{right:91.66666667%}.col-xs-pull-10{right:83.33333333%}.col-xs-pull-9{right:75%}.col-xs-pull-8{right:66.66666667%}.col-xs-pull-7{right:58.33333333%}.col-xs-pull-6{right:50%}.col-xs-pull-5{right:41.66666667%}.col-xs-pull-4{right:33.33333333%}.col-xs-pull-3{right:25%}.col-xs-pull-2{right:16.66666667%}.col-xs-pull-1{right:8.33333333%}.col-xs-pull-0{right:auto}.col-xs-push-12{left:100%}.col-xs-push-11{left:91.66666667%}.col-xs-push-10{left:83.33333333%}.col-xs-push-9{left:75%}.col-xs-push-8{left:66.66666667%}.col-xs-push-7{left:58.33333333%}.col-xs-push-6{left:50%}.col-xs-push-5{left:41.66666667%}.col-xs-push-4{left:33.33333333%}.col-xs-push-3{left:25%}.col-xs-push-2{left:16.66666667%}.col-xs-push-1{left:8.33333333%}.col-xs-push-0{left:auto}.col-xs-offset-12{margin-left:100%}.col-xs-offset-11{margin-left:91.66666667%}.col-xs-offset-10{margin-left:83.33333333%}.col-xs-offset-9{margin-left:75%}.col-xs-offset-8{margin-left:66.66666667%}.col-xs-offset-7{margin-left:58.33333333%}.col-xs-offset-6{margin-left:50%}.col-xs-offset-5{margin-left:41.66666667%}.col-xs-offset-4{margin-left:33.33333333%}.col-xs-offset-3{margin-left:25%}.col-xs-offset-2{margin-left:16.66666667%}.col-xs-offset-1{margin-left:8.33333333%}.col-xs-offset-0{margin-left:0}@media (min-width:768px){.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9{float:left}.col-sm-12{width:100%}.col-sm-11{width:91.66666667%}.col-sm-10{width:83.33333333%}.col-sm-9{width:75%}.col-sm-8{width:66.66666667%}.col-sm-7{width:58.33333333%}.col-sm-6{width:50%}.col-sm-5{width:41.66666667%}.col-sm-4{width:33.33333333%}.col-sm-3{width:25%}.col-sm-2{width:16.66666667%}.col-sm-1{width:8.33333333%}.col-sm-pull-12{right:100%}.col-sm-pull-11{right:91.66666667%}.col-sm-pull-10{right:83.33333333%}.col-sm-pull-9{right:75%}.col-sm-pull-8{right:66.66666667%}.col-sm-pull-7{right:58.33333333%}.col-sm-pull-6{right:50%}.col-sm-pull-5{right:41.66666667%}.col-sm-pull-4{right:33.33333333%}.col-sm-pull-3{right:25%}.col-sm-pull-2{right:16.66666667%}.col-sm-pull-1{right:8.33333333%}.col-sm-pull-0{right:auto}.col-sm-push-12{left:100%}.col-sm-push-11{left:91.66666667%}.col-sm-push-10{left:83.33333333%}.col-sm-push-9{left:75%}.col-sm-push-8{left:66.66666667%}.col-sm-push-7{left:58.33333333%}.col-sm-push-6{left:50%}.col-sm-push-5{left:41.66666667%}.col-sm-push-4{left:33.33333333%}.col-sm-push-3{left:25%}.col-sm-push-2{left:16.66666667%}.col-sm-push-1{left:8.33333333%}.col-sm-push-0{left:auto}.col-sm-offset-12{margin-left:100%}.col-sm-offset-11{margin-left:91.66666667%}.col-sm-offset-10{margin-left:83.33333333%}.col-sm-offset-9{margin-left:75%}.col-sm-offset-8{margin-left:66.66666667%}.col-sm-offset-7{margin-left:58.33333333%}.col-sm-offset-6{margin-left:50%}.col-sm-offset-5{margin-left:41.66666667%}.col-sm-offset-4{margin-left:33.33333333%}.col-sm-offset-3{margin-left:25%}.col-sm-offset-2{margin-left:16.66666667%}.col-sm-offset-1{margin-left:8.33333333%}.col-sm-offset-0{margin-left:0}}@media (min-width:992px){.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9{float:left}.col-md-12{width:100%}.col-md-11{width:91.66666667%}.col-md-10{width:83.33333333%}.col-md-9{width:75%}.col-md-8{width:66.66666667%}.col-md-7{width:58.33333333%}.col-md-6{width:50%}.col-md-5{width:41.66666667%}.col-md-4{width:33.33333333%}.col-md-3{width:25%}.col-md-2{width:16.66666667%}.col-md-1{width:8.33333333%}.col-md-pull-12{right:100%}.col-md-pull-11{right:91.66666667%}.col-md-pull-10{right:83.33333333%}.col-md-pull-9{right:75%}.col-md-pull-8{right:66.66666667%}.col-md-pull-7{right:58.33333333%}.col-md-pull-6{right:50%}.col-md-pull-5{right:41.66666667%}.col-md-pull-4{right:33.33333333%}.col-md-pull-3{right:25%}.col-md-pull-2{right:16.66666667%}.col-md-pull-1{right:8.33333333%}.col-md-pull-0{right:auto}.col-md-push-12{left:100%}.col-md-push-11{left:91.66666667%}.col-md-push-10{left:83.33333333%}.col-md-push-9{left:75%}.col-md-push-8{left:66.66666667%}.col-md-push-7{left:58.33333333%}.col-md-push-6{left:50%}.col-md-push-5{left:41.66666667%}.col-md-push-4{left:33.33333333%}.col-md-push-3{left:25%}.col-md-push-2{left:16.66666667%}.col-md-push-1{left:8.33333333%}.col-md-push-0{left:auto}.col-md-offset-12{margin-left:100%}.col-md-offset-11{margin-left:91.66666667%}.col-md-offset-10{margin-left:83.33333333%}.col-md-offset-9{margin-left:75%}.col-md-offset-8{margin-left:66.66666667%}.col-md-offset-7{margin-left:58.33333333%}.col-md-offset-6{margin-left:50%}.col-md-offset-5{margin-left:41.66666667%}.col-md-offset-4{margin-left:33.33333333%}.col-md-offset-3{margin-left:25%}.col-md-offset-2{margin-left:16.66666667%}.col-md-offset-1{margin-left:8.33333333%}.col-md-offset-0{margin-left:0}}@media (min-width:1200px){.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9{float:left}.col-lg-12{width:100%}.col-lg-11{width:91.66666667%}.col-lg-10{width:83.33333333%}.col-lg-9{width:75%}.col-lg-8{width:66.66666667%}.col-lg-7{width:58.33333333%}.col-lg-6{width:50%}.col-lg-5{width:41.66666667%}.col-lg-4{width:33.33333333%}.col-lg-3{width:25%}.col-lg-2{width:16.66666667%}.col-lg-1{width:8.33333333%}.col-lg-pull-12{right:100%}.col-lg-pull-11{right:91.66666667%}.col-lg-pull-10{right:83.33333333%}.col-lg-pull-9{right:75%}.col-lg-pull-8{right:66.66666667%}.col-lg-pull-7{right:58.33333333%}.col-lg-pull-6{right:50%}.col-lg-pull-5{right:41.66666667%}.col-lg-pull-4{right:33.33333333%}.col-lg-pull-3{right:25%}.col-lg-pull-2{right:16.66666667%}.col-lg-pull-1{right:8.33333333%}.col-lg-pull-0{right:auto}.col-lg-push-12{left:100%}.col-lg-push-11{left:91.66666667%}.col-lg-push-10{left:83.33333333%}.col-lg-push-9{left:75%}.col-lg-push-8{left:66.66666667%}.col-lg-push-7{left:58.33333333%}.col-lg-push-6{left:50%}.col-lg-push-5{left:41.66666667%}.col-lg-push-4{left:33.33333333%}.col-lg-push-3{left:25%}.col-lg-push-2{left:16.66666667%}.col-lg-push-1{left:8.33333333%}.col-lg-push-0{left:auto}.col-lg-offset-12{margin-left:100%}.col-lg-offset-11{margin-left:91.66666667%}.col-lg-offset-10{margin-left:83.33333333%}.col-lg-offset-9{margin-left:75%}.col-lg-offset-8{margin-left:66.66666667%}.col-lg-offset-7{margin-left:58.33333333%}.col-lg-offset-6{margin-left:50%}.col-lg-offset-5{margin-left:41.66666667%}.col-lg-offset-4{margin-left:33.33333333%}.col-lg-offset-3{margin-left:25%}.col-lg-offset-2{margin-left:16.66666667%}.col-lg-offset-1{margin-left:8.33333333%}.col-lg-offset-0{margin-left:0}}table{background-color:transparent}caption{padding-top:8px;padding-bottom:8px;color:#777;text-align:left}th{text-align:left}.table{width:100%;max-width:100%;margin-bottom:20px}.table>tbody>tr>td,.table>tbody>tr>th,.table>tfoot>tr>td,.table>tfoot>tr>th,.table>thead>tr>td,.table>thead>tr>th{padding:8px;line-height:1.42857143;vertical-align:top;border-top:1px solid #ddd}.table>thead>tr>th{vertical-align:bottom;border-bottom:2px solid #ddd}.table>caption+thead>tr:first-child>td,.table>caption+thead>tr:first-child>th,.table>colgroup+thead>tr:first-child>td,.table>colgroup+thead>tr:first-child>th,.table>thead:first-child>tr:first-child>td,.table>thead:first-child>tr:first-child>th{border-top:0}.table>tbody+tbody{border-top:2px solid #ddd}.table .table{background-color:#fff}.table-condensed>tbody>tr>td,.table-condensed>tbody>tr>th,.table-condensed>tfoot>tr>td,.table-condensed>tfoot>tr>th,.table-condensed>thead>tr>td,.table-condensed>thead>tr>th{padding:5px}.table-bordered{border:1px solid #ddd}.table-bordered>tbody>tr>td,.table-bordered>tbody>tr>th,.table-bordered>tfoot>tr>td,.table-bordered>tfoot>tr>th,.table-bordered>thead>tr>td,.table-bordered>thead>tr>th{border:1px solid #ddd}.table-bordered>thead>tr>td,.table-bordered>thead>tr>th{border-bottom-width:2px}.table-striped>tbody>tr:nth-of-type(odd){background-color:#f9f9f9}.table-hover>tbody>tr:hover{background-color:#f5f5f5}table col[class*=col-]{position:static;display:table-column;float:none}table td[class*=col-],table th[class*=col-]{position:static;display:table-cell;float:none}.table>tbody>tr.active>td,.table>tbody>tr.active>th,.table>tbody>tr>td.active,.table>tbody>tr>th.active,.table>tfoot>tr.active>td,.table>tfoot>tr.active>th,.table>tfoot>tr>td.active,.table>tfoot>tr>th.active,.table>thead>tr.active>td,.table>thead>tr.active>th,.table>thead>tr>td.active,.table>thead>tr>th.active{background-color:#f5f5f5}.table-hover>tbody>tr.active:hover>td,.table-hover>tbody>tr.active:hover>th,.table-hover>tbody>tr:hover>.active,.table-hover>tbody>tr>td.active:hover,.table-hover>tbody>tr>th.active:hover{background-color:#e8e8e8}.table>tbody>tr.success>td,.table>tbody>tr.success>th,.table>tbody>tr>td.success,.table>tbody>tr>th.success,.table>tfoot>tr.success>td,.table>tfoot>tr.success>th,.table>tfoot>tr>td.success,.table>tfoot>tr>th.success,.table>thead>tr.success>td,.table>thead>tr.success>th,.table>thead>tr>td.success,.table>thead>tr>th.success{background-color:#dff0d8}.table-hover>tbody>tr.success:hover>td,.table-hover>tbody>tr.success:hover>th,.table-hover>tbody>tr:hover>.success,.table-hover>tbody>tr>td.success:hover,.table-hover>tbody>tr>th.success:hover{background-color:#d0e9c6}.table>tbody>tr.info>td,.table>tbody>tr.info>th,.table>tbody>tr>td.info,.table>tbody>tr>th.info,.table>tfoot>tr.info>td,.table>tfoot>tr.info>th,.table>tfoot>tr>td.info,.table>tfoot>tr>th.info,.table>thead>tr.info>td,.table>thead>tr.info>th,.table>thead>tr>td.info,.table>thead>tr>th.info{background-color:#d9edf7}.table-hover>tbody>tr.info:hover>td,.table-hover>tbody>tr.info:hover>th,.table-hover>tbody>tr:hover>.info,.table-hover>tbody>tr>td.info:hover,.table-hover>tbody>tr>th.info:hover{background-color:#c4e3f3}.table>tbody>tr.warning>td,.table>tbody>tr.warning>th,.table>tbody>tr>td.warning,.table>tbody>tr>th.warning,.table>tfoot>tr.warning>td,.table>tfoot>tr.warning>th,.table>tfoot>tr>td.warning,.table>tfoot>tr>th.warning,.table>thead>tr.warning>td,.table>thead>tr.warning>th,.table>thead>tr>td.warning,.table>thead>tr>th.warning{background-color:#fcf8e3}.table-hover>tbody>tr.warning:hover>td,.table-hover>tbody>tr.warning:hover>th,.table-hover>tbody>tr:hover>.warning,.table-hover>tbody>tr>td.warning:hover,.table-hover>tbody>tr>th.warning:hover{background-color:#faf2cc}.table>tbody>tr.danger>td,.table>tbody>tr.danger>th,.table>tbody>tr>td.danger,.table>tbody>tr>th.danger,.table>tfoot>tr.danger>td,.table>tfoot>tr.danger>th,.table>tfoot>tr>td.danger,.table>tfoot>tr>th.danger,.table>thead>tr.danger>td,.table>thead>tr.danger>th,.table>thead>tr>td.danger,.table>thead>tr>th.danger{background-color:#f2dede}.table-hover>tbody>tr.danger:hover>td,.table-hover>tbody>tr.danger:hover>th,.table-hover>tbody>tr:hover>.danger,.table-hover>tbody>tr>td.danger:hover,.table-hover>tbody>tr>th.danger:hover{background-color:#ebcccc}.table-responsive{min-height:.01%;overflow-x:auto}@media screen and (max-width:767px){.table-responsive{width:100%;margin-bottom:15px;overflow-y:hidden;-ms-overflow-style:-ms-autohiding-scrollbar;border:1px solid #ddd}.table-responsive>.table{margin-bottom:0}.table-responsive>.table>tbody>tr>td,.table-responsive>.table>tbody>tr>th,.table-responsive>.table>tfoot>tr>td,.table-responsive>.table>tfoot>tr>th,.table-responsive>.table>thead>tr>td,.table-responsive>.table>thead>tr>th{white-space:nowrap}.table-responsive>.table-bordered{border:0}.table-responsive>.table-bordered>tbody>tr>td:first-child,.table-responsive>.table-bordered>tbody>tr>th:first-child,.table-responsive>.table-bordered>tfoot>tr>td:first-child,.table-responsive>.table-bordered>tfoot>tr>th:first-child,.table-responsive>.table-bordered>thead>tr>td:first-child,.table-responsive>.table-bordered>thead>tr>th:first-child{border-left:0}.table-responsive>.table-bordered>tbody>tr>td:last-child,.table-responsive>.table-bordered>tbody>tr>th:last-child,.table-responsive>.table-bordered>tfoot>tr>td:last-child,.table-responsive>.table-bordered>tfoot>tr>th:last-child,.table-responsive>.table-bordered>thead>tr>td:last-child,.table-responsive>.table-bordered>thead>tr>th:last-child{border-right:0}.table-responsive>.table-bordered>tbody>tr:last-child>td,.table-responsive>.table-bordered>tbody>tr:last-child>th,.table-responsive>.table-bordered>tfoot>tr:last-child>td,.table-responsive>.table-bordered>tfoot>tr:last-child>th{border-bottom:0}}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;padding:0;margin-bottom:20px;font-size:21px;line-height:inherit;color:#333;border:0;border-bottom:1px solid #e5e5e5}label{display:inline-block;max-width:100%;margin-bottom:5px;font-weight:700}input[type=search]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}input[type=checkbox],input[type=radio]{margin:4px 0 0;margin-top:1px\9;line-height:normal}input[type=file]{display:block}input[type=range]{display:block;width:100%}select[multiple],select[size]{height:auto}input[type=file]:focus,input[type=checkbox]:focus,input[type=radio]:focus{outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}output{display:block;padding-top:7px;font-size:14px;line-height:1.42857143;color:#555}.form-control{display:block;width:100%;height:34px;padding:6px 12px;font-size:14px;line-height:1.42857143;color:#555;background-color:#fff;background-image:none;border:1px solid #ccc;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075);-webkit-transition:border-color ease-in-out .15s,-webkit-box-shadow ease-in-out .15s;-o-transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s}.form-control:focus{border-color:#66afe9;outline:0;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6);box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6)}.form-control::-moz-placeholder{color:#999;opacity:1}.form-control:-ms-input-placeholder{color:#999}.form-control::-webkit-input-placeholder{color:#999}.form-control::-ms-expand{background-color:transparent;border:0}.form-control[disabled],.form-control[readonly],fieldset[disabled] .form-control{background-color:#eee;opacity:1}.form-control[disabled],fieldset[disabled] .form-control{cursor:not-allowed}textarea.form-control{height:auto}input[type=search]{-webkit-appearance:none}@media screen and (-webkit-min-device-pixel-ratio:0){input[type=date].form-control,input[type=time].form-control,input[type=datetime-local].form-control,input[type=month].form-control{line-height:34px}.input-group-sm input[type=date],.input-group-sm input[type=time],.input-group-sm input[type=datetime-local],.input-group-sm input[type=month],input[type=date].input-sm,input[type=time].input-sm,input[type=datetime-local].input-sm,input[type=month].input-sm{line-height:30px}.input-group-lg input[type=date],.input-group-lg input[type=time],.input-group-lg input[type=datetime-local],.input-group-lg input[type=month],input[type=date].input-lg,input[type=time].input-lg,input[type=datetime-local].input-lg,input[type=month].input-lg{line-height:46px}}.form-group{margin-bottom:15px}.checkbox,.radio{position:relative;display:block;margin-top:10px;margin-bottom:10px}.checkbox label,.radio label{min-height:20px;padding-left:20px;margin-bottom:0;font-weight:400;cursor:pointer}.checkbox input[type=checkbox],.checkbox-inline input[type=checkbox],.radio input[type=radio],.radio-inline input[type=radio]{position:absolute;margin-top:4px\9;margin-left:-20px}.checkbox+.checkbox,.radio+.radio{margin-top:-5px}.checkbox-inline,.radio-inline{position:relative;display:inline-block;padding-left:20px;margin-bottom:0;font-weight:400;vertical-align:middle;cursor:pointer}.checkbox-inline+.checkbox-inline,.radio-inline+.radio-inline{margin-top:0;margin-left:10px}fieldset[disabled] input[type=checkbox],fieldset[disabled] input[type=radio],input[type=checkbox].disabled,input[type=checkbox][disabled],input[type=radio].disabled,input[type=radio][disabled]{cursor:not-allowed}.checkbox-inline.disabled,.radio-inline.disabled,fieldset[disabled] .checkbox-inline,fieldset[disabled] .radio-inline{cursor:not-allowed}.checkbox.disabled label,.radio.disabled label,fieldset[disabled] .checkbox label,fieldset[disabled] .radio label{cursor:not-allowed}.form-control-static{min-height:34px;padding-top:7px;padding-bottom:7px;margin-bottom:0}.form-control-static.input-lg,.form-control-static.input-sm{padding-right:0;padding-left:0}.input-sm{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-sm{height:30px;line-height:30px}select[multiple].input-sm,textarea.input-sm{height:auto}.form-group-sm .form-control{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}.form-group-sm select.form-control{height:30px;line-height:30px}.form-group-sm select[multiple].form-control,.form-group-sm textarea.form-control{height:auto}.form-group-sm .form-control-static{height:30px;min-height:32px;padding:6px 10px;font-size:12px;line-height:1.5}.input-lg{height:46px;padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}select.input-lg{height:46px;line-height:46px}select[multiple].input-lg,textarea.input-lg{height:auto}.form-group-lg .form-control{height:46px;padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}.form-group-lg select.form-control{height:46px;line-height:46px}.form-group-lg select[multiple].form-control,.form-group-lg textarea.form-control{height:auto}.form-group-lg .form-control-static{height:46px;min-height:38px;padding:11px 16px;font-size:18px;line-height:1.3333333}.has-feedback{position:relative}.has-feedback .form-control{padding-right:42.5px}.form-control-feedback{position:absolute;top:0;right:0;z-index:2;display:block;width:34px;height:34px;line-height:34px;text-align:center;pointer-events:none}.form-group-lg .form-control+.form-control-feedback,.input-group-lg+.form-control-feedback,.input-lg+.form-control-feedback{width:46px;height:46px;line-height:46px}.form-group-sm .form-control+.form-control-feedback,.input-group-sm+.form-control-feedback,.input-sm+.form-control-feedback{width:30px;height:30px;line-height:30px}.has-success .checkbox,.has-success .checkbox-inline,.has-success .control-label,.has-success .help-block,.has-success .radio,.has-success .radio-inline,.has-success.checkbox label,.has-success.checkbox-inline label,.has-success.radio label,.has-success.radio-inline label{color:#3c763d}.has-success .form-control{border-color:#3c763d;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-success .form-control:focus{border-color:#2b542c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168}.has-success .input-group-addon{color:#3c763d;background-color:#dff0d8;border-color:#3c763d}.has-success .form-control-feedback{color:#3c763d}.has-warning .checkbox,.has-warning .checkbox-inline,.has-warning .control-label,.has-warning .help-block,.has-warning .radio,.has-warning .radio-inline,.has-warning.checkbox label,.has-warning.checkbox-inline label,.has-warning.radio label,.has-warning.radio-inline label{color:#8a6d3b}.has-warning .form-control{border-color:#8a6d3b;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-warning .form-control:focus{border-color:#66512c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b}.has-warning .input-group-addon{color:#8a6d3b;background-color:#fcf8e3;border-color:#8a6d3b}.has-warning .form-control-feedback{color:#8a6d3b}.has-error .checkbox,.has-error .checkbox-inline,.has-error .control-label,.has-error .help-block,.has-error .radio,.has-error .radio-inline,.has-error.checkbox label,.has-error.checkbox-inline label,.has-error.radio label,.has-error.radio-inline label{color:#a94442}.has-error .form-control{border-color:#a94442;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-error .form-control:focus{border-color:#843534;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483}.has-error .input-group-addon{color:#a94442;background-color:#f2dede;border-color:#a94442}.has-error .form-control-feedback{color:#a94442}.has-feedback label~.form-control-feedback{top:25px}.has-feedback label.sr-only~.form-control-feedback{top:0}.help-block{display:block;margin-top:5px;margin-bottom:10px;color:#737373}@media (min-width:768px){.form-inline .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .form-control-static{display:inline-block}.form-inline .input-group{display:inline-table;vertical-align:middle}.form-inline .input-group .form-control,.form-inline .input-group .input-group-addon,.form-inline .input-group .input-group-btn{width:auto}.form-inline .input-group>.form-control{width:100%}.form-inline .control-label{margin-bottom:0;vertical-align:middle}.form-inline .checkbox,.form-inline .radio{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.form-inline .checkbox label,.form-inline .radio label{padding-left:0}.form-inline .checkbox input[type=checkbox],.form-inline .radio input[type=radio]{position:relative;margin-left:0}.form-inline .has-feedback .form-control-feedback{top:0}}.form-horizontal .checkbox,.form-horizontal .checkbox-inline,.form-horizontal .radio,.form-horizontal .radio-inline{padding-top:7px;margin-top:0;margin-bottom:0}.form-horizontal .checkbox,.form-horizontal .radio{min-height:27px}.form-horizontal .form-group{margin-right:-15px;margin-left:-15px}@media (min-width:768px){.form-horizontal .control-label{padding-top:7px;margin-bottom:0;text-align:right}}.form-horizontal .has-feedback .form-control-feedback{right:15px}@media (min-width:768px){.form-horizontal .form-group-lg .control-label{padding-top:11px;font-size:18px}}@media (min-width:768px){.form-horizontal .form-group-sm .control-label{padding-top:6px;font-size:12px}}.btn{display:inline-block;padding:6px 12px;margin-bottom:0;font-size:14px;font-weight:400;line-height:1.42857143;text-align:center;white-space:nowrap;vertical-align:middle;-ms-touch-action:manipulation;touch-action:manipulation;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;background-image:none;border:1px solid transparent;border-radius:4px}.btn.active.focus,.btn.active:focus,.btn.focus,.btn:active.focus,.btn:active:focus,.btn:focus{outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}.btn.focus,.btn:focus,.btn:hover{color:#333;text-decoration:none}.btn.active,.btn:active{background-image:none;outline:0;-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn.disabled,.btn[disabled],fieldset[disabled] .btn{cursor:not-allowed;filter:alpha(opacity=65);-webkit-box-shadow:none;box-shadow:none;opacity:.65}a.btn.disabled,fieldset[disabled] a.btn{pointer-events:none}.btn-default{color:#333;background-color:#fff;border-color:#ccc}.btn-default.focus,.btn-default:focus{color:#333;background-color:#e6e6e6;border-color:#8c8c8c}.btn-default:hover{color:#333;background-color:#e6e6e6;border-color:#adadad}.btn-default.active,.btn-default:active,.open>.dropdown-toggle.btn-default{color:#333;background-color:#e6e6e6;border-color:#adadad}.btn-default.active.focus,.btn-default.active:focus,.btn-default.active:hover,.btn-default:active.focus,.btn-default:active:focus,.btn-default:active:hover,.open>.dropdown-toggle.btn-default.focus,.open>.dropdown-toggle.btn-default:focus,.open>.dropdown-toggle.btn-default:hover{color:#333;background-color:#d4d4d4;border-color:#8c8c8c}.btn-default.active,.btn-default:active,.open>.dropdown-toggle.btn-default{background-image:none}.btn-default.disabled.focus,.btn-default.disabled:focus,.btn-default.disabled:hover,.btn-default[disabled].focus,.btn-default[disabled]:focus,.btn-default[disabled]:hover,fieldset[disabled] .btn-default.focus,fieldset[disabled] .btn-default:focus,fieldset[disabled] .btn-default:hover{background-color:#fff;border-color:#ccc}.btn-default .badge{color:#fff;background-color:#333}.btn-primary{color:#fff;background-color:#337ab7;border-color:#2e6da4}.btn-primary.focus,.btn-primary:focus{color:#fff;background-color:#286090;border-color:#122b40}.btn-primary:hover{color:#fff;background-color:#286090;border-color:#204d74}.btn-primary.active,.btn-primary:active,.open>.dropdown-toggle.btn-primary{color:#fff;background-color:#286090;border-color:#204d74}.btn-primary.active.focus,.btn-primary.active:focus,.btn-primary.active:hover,.btn-primary:active.focus,.btn-primary:active:focus,.btn-primary:active:hover,.open>.dropdown-toggle.btn-primary.focus,.open>.dropdown-toggle.btn-primary:focus,.open>.dropdown-toggle.btn-primary:hover{color:#fff;background-color:#204d74;border-color:#122b40}.btn-primary.active,.btn-primary:active,.open>.dropdown-toggle.btn-primary{background-image:none}.btn-primary.disabled.focus,.btn-primary.disabled:focus,.btn-primary.disabled:hover,.btn-primary[disabled].focus,.btn-primary[disabled]:focus,.btn-primary[disabled]:hover,fieldset[disabled] .btn-primary.focus,fieldset[disabled] .btn-primary:focus,fieldset[disabled] .btn-primary:hover{background-color:#337ab7;border-color:#2e6da4}.btn-primary .badge{color:#337ab7;background-color:#fff}.btn-success{color:#fff;background-color:#5cb85c;border-color:#4cae4c}.btn-success.focus,.btn-success:focus{color:#fff;background-color:#449d44;border-color:#255625}.btn-success:hover{color:#fff;background-color:#449d44;border-color:#398439}.btn-success.active,.btn-success:active,.open>.dropdown-toggle.btn-success{color:#fff;background-color:#449d44;border-color:#398439}.btn-success.active.focus,.btn-success.active:focus,.btn-success.active:hover,.btn-success:active.focus,.btn-success:active:focus,.btn-success:active:hover,.open>.dropdown-toggle.btn-success.focus,.open>.dropdown-toggle.btn-success:focus,.open>.dropdown-toggle.btn-success:hover{color:#fff;background-color:#398439;border-color:#255625}.btn-success.active,.btn-success:active,.open>.dropdown-toggle.btn-success{background-image:none}.btn-success.disabled.focus,.btn-success.disabled:focus,.btn-success.disabled:hover,.btn-success[disabled].focus,.btn-success[disabled]:focus,.btn-success[disabled]:hover,fieldset[disabled] .btn-success.focus,fieldset[disabled] .btn-success:focus,fieldset[disabled] .btn-success:hover{background-color:#5cb85c;border-color:#4cae4c}.btn-success .badge{color:#5cb85c;background-color:#fff}.btn-info{color:#fff;background-color:#5bc0de;border-color:#46b8da}.btn-info.focus,.btn-info:focus{color:#fff;background-color:#31b0d5;border-color:#1b6d85}.btn-info:hover{color:#fff;background-color:#31b0d5;border-color:#269abc}.btn-info.active,.btn-info:active,.open>.dropdown-toggle.btn-info{color:#fff;background-color:#31b0d5;border-color:#269abc}.btn-info.active.focus,.btn-info.active:focus,.btn-info.active:hover,.btn-info:active.focus,.btn-info:active:focus,.btn-info:active:hover,.open>.dropdown-toggle.btn-info.focus,.open>.dropdown-toggle.btn-info:focus,.open>.dropdown-toggle.btn-info:hover{color:#fff;background-color:#269abc;border-color:#1b6d85}.btn-info.active,.btn-info:active,.open>.dropdown-toggle.btn-info{background-image:none}.btn-info.disabled.focus,.btn-info.disabled:focus,.btn-info.disabled:hover,.btn-info[disabled].focus,.btn-info[disabled]:focus,.btn-info[disabled]:hover,fieldset[disabled] .btn-info.focus,fieldset[disabled] .btn-info:focus,fieldset[disabled] .btn-info:hover{background-color:#5bc0de;border-color:#46b8da}.btn-info .badge{color:#5bc0de;background-color:#fff}.btn-warning{color:#fff;background-color:#f0ad4e;border-color:#eea236}.btn-warning.focus,.btn-warning:focus{color:#fff;background-color:#ec971f;border-color:#985f0d}.btn-warning:hover{color:#fff;background-color:#ec971f;border-color:#d58512}.btn-warning.active,.btn-warning:active,.open>.dropdown-toggle.btn-warning{color:#fff;background-color:#ec971f;border-color:#d58512}.btn-warning.active.focus,.btn-warning.active:focus,.btn-warning.active:hover,.btn-warning:active.focus,.btn-warning:active:focus,.btn-warning:active:hover,.open>.dropdown-toggle.btn-warning.focus,.open>.dropdown-toggle.btn-warning:focus,.open>.dropdown-toggle.btn-warning:hover{color:#fff;background-color:#d58512;border-color:#985f0d}.btn-warning.active,.btn-warning:active,.open>.dropdown-toggle.btn-warning{background-image:none}.btn-warning.disabled.focus,.btn-warning.disabled:focus,.btn-warning.disabled:hover,.btn-warning[disabled].focus,.btn-warning[disabled]:focus,.btn-warning[disabled]:hover,fieldset[disabled] .btn-warning.focus,fieldset[disabled] .btn-warning:focus,fieldset[disabled] .btn-warning:hover{background-color:#f0ad4e;border-color:#eea236}.btn-warning .badge{color:#f0ad4e;background-color:#fff}.btn-danger{color:#fff;background-color:#d9534f;border-color:#d43f3a}.btn-danger.focus,.btn-danger:focus{color:#fff;background-color:#c9302c;border-color:#761c19}.btn-danger:hover{color:#fff;background-color:#c9302c;border-color:#ac2925}.btn-danger.active,.btn-danger:active,.open>.dropdown-toggle.btn-danger{color:#fff;background-color:#c9302c;border-color:#ac2925}.btn-danger.active.focus,.btn-danger.active:focus,.btn-danger.active:hover,.btn-danger:active.focus,.btn-danger:active:focus,.btn-danger:active:hover,.open>.dropdown-toggle.btn-danger.focus,.open>.dropdown-toggle.btn-danger:focus,.open>.dropdown-toggle.btn-danger:hover{color:#fff;background-color:#ac2925;border-color:#761c19}.btn-danger.active,.btn-danger:active,.open>.dropdown-toggle.btn-danger{background-image:none}.btn-danger.disabled.focus,.btn-danger.disabled:focus,.btn-danger.disabled:hover,.btn-danger[disabled].focus,.btn-danger[disabled]:focus,.btn-danger[disabled]:hover,fieldset[disabled] .btn-danger.focus,fieldset[disabled] .btn-danger:focus,fieldset[disabled] .btn-danger:hover{background-color:#d9534f;border-color:#d43f3a}.btn-danger .badge{color:#d9534f;background-color:#fff}.btn-link{font-weight:400;color:#337ab7;border-radius:0}.btn-link,.btn-link.active,.btn-link:active,.btn-link[disabled],fieldset[disabled] .btn-link{background-color:transparent;-webkit-box-shadow:none;box-shadow:none}.btn-link,.btn-link:active,.btn-link:focus,.btn-link:hover{border-color:transparent}.btn-link:focus,.btn-link:hover{color:#23527c;text-decoration:underline;background-color:transparent}.btn-link[disabled]:focus,.btn-link[disabled]:hover,fieldset[disabled] .btn-link:focus,fieldset[disabled] .btn-link:hover{color:#777;text-decoration:none}.btn-group-lg>.btn,.btn-lg{padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}.btn-group-sm>.btn,.btn-sm{padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}.btn-group-xs>.btn,.btn-xs{padding:1px 5px;font-size:12px;line-height:1.5;border-radius:3px}.btn-block{display:block;width:100%}.btn-block+.btn-block{margin-top:5px}input[type=button].btn-block,input[type=reset].btn-block,input[type=submit].btn-block{width:100%}.fade{opacity:0;-webkit-transition:opacity .15s linear;-o-transition:opacity .15s linear;transition:opacity .15s linear}.fade.in{opacity:1}.collapse{display:none}.collapse.in{display:block}tr.collapse.in{display:table-row}tbody.collapse.in{display:table-row-group}.collapsing{position:relative;height:0;overflow:hidden;-webkit-transition-timing-function:ease;-o-transition-timing-function:ease;transition-timing-function:ease;-webkit-transition-duration:.35s;-o-transition-duration:.35s;transition-duration:.35s;-webkit-transition-property:height,visibility;-o-transition-property:height,visibility;transition-property:height,visibility}.caret{display:inline-block;width:0;height:0;margin-left:2px;vertical-align:middle;border-top:4px dashed;border-top:4px solid\9;border-right:4px solid transparent;border-left:4px solid transparent}.dropdown,.dropup{position:relative}.dropdown-toggle:focus{outline:0}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:160px;padding:5px 0;margin:2px 0 0;font-size:14px;text-align:left;list-style:none;background-color:#fff;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid #ccc;border:1px solid rgba(0,0,0,.15);border-radius:4px;-webkit-box-shadow:0 6px 12px rgba(0,0,0,.175);box-shadow:0 6px 12px rgba(0,0,0,.175)}.dropdown-menu.pull-right{right:0;left:auto}.dropdown-menu .divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.dropdown-menu>li>a{display:block;padding:3px 20px;clear:both;font-weight:400;line-height:1.42857143;color:#333;white-space:nowrap}.dropdown-menu>li>a:focus,.dropdown-menu>li>a:hover{color:#262626;text-decoration:none;background-color:#f5f5f5}.dropdown-menu>.active>a,.dropdown-menu>.active>a:focus,.dropdown-menu>.active>a:hover{color:#fff;text-decoration:none;background-color:#337ab7;outline:0}.dropdown-menu>.disabled>a,.dropdown-menu>.disabled>a:focus,.dropdown-menu>.disabled>a:hover{color:#777}.dropdown-menu>.disabled>a:focus,.dropdown-menu>.disabled>a:hover{text-decoration:none;cursor:not-allowed;background-color:transparent;background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.open>.dropdown-menu{display:block}.open>a{outline:0}.dropdown-menu-right{right:0;left:auto}.dropdown-menu-left{right:auto;left:0}.dropdown-header{display:block;padding:3px 20px;font-size:12px;line-height:1.42857143;color:#777;white-space:nowrap}.dropdown-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:990}.pull-right>.dropdown-menu{right:0;left:auto}.dropup .caret,.navbar-fixed-bottom .dropdown .caret{content:"";border-top:0;border-bottom:4px dashed;border-bottom:4px solid\9}.dropup .dropdown-menu,.navbar-fixed-bottom .dropdown .dropdown-menu{top:auto;bottom:100%;margin-bottom:2px}@media (min-width:768px){.navbar-right .dropdown-menu{right:0;left:auto}.navbar-right .dropdown-menu-left{right:auto;left:0}}.btn-group,.btn-group-vertical{position:relative;display:inline-block;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;float:left}.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group-vertical>.btn:hover,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus,.btn-group>.btn:hover{z-index:2}.btn-group .btn+.btn,.btn-group .btn+.btn-group,.btn-group .btn-group+.btn,.btn-group .btn-group+.btn-group{margin-left:-1px}.btn-toolbar{margin-left:-5px}.btn-toolbar .btn,.btn-toolbar .btn-group,.btn-toolbar .input-group{float:left}.btn-toolbar>.btn,.btn-toolbar>.btn-group,.btn-toolbar>.input-group{margin-left:5px}.btn-group>.btn:not(:first-child):not(:last-child):not(.dropdown-toggle){border-radius:0}.btn-group>.btn:first-child{margin-left:0}.btn-group>.btn:first-child:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn:last-child:not(:first-child),.btn-group>.dropdown-toggle:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.btn-group>.btn-group{float:left}.btn-group>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:last-child:not(:first-child)>.btn:first-child{border-top-left-radius:0;border-bottom-left-radius:0}.btn-group .dropdown-toggle:active,.btn-group.open .dropdown-toggle{outline:0}.btn-group>.btn+.dropdown-toggle{padding-right:8px;padding-left:8px}.btn-group>.btn-lg+.dropdown-toggle{padding-right:12px;padding-left:12px}.btn-group.open .dropdown-toggle{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn-group.open .dropdown-toggle.btn-link{-webkit-box-shadow:none;box-shadow:none}.btn .caret{margin-left:0}.btn-lg .caret{border-width:5px 5px 0;border-bottom-width:0}.dropup .btn-lg .caret{border-width:0 5px 5px}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group,.btn-group-vertical>.btn-group>.btn{display:block;float:none;width:100%;max-width:100%}.btn-group-vertical>.btn-group>.btn{float:none}.btn-group-vertical>.btn+.btn,.btn-group-vertical>.btn+.btn-group,.btn-group-vertical>.btn-group+.btn,.btn-group-vertical>.btn-group+.btn-group{margin-top:-1px;margin-left:0}.btn-group-vertical>.btn:not(:first-child):not(:last-child){border-radius:0}.btn-group-vertical>.btn:first-child:not(:last-child){border-top-left-radius:4px;border-top-right-radius:4px;border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn:last-child:not(:first-child){border-top-left-radius:0;border-top-right-radius:0;border-bottom-right-radius:4px;border-bottom-left-radius:4px}.btn-group-vertical>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group-vertical>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group-vertical>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:last-child:not(:first-child)>.btn:first-child{border-top-left-radius:0;border-top-right-radius:0}.btn-group-justified{display:table;width:100%;table-layout:fixed;border-collapse:separate}.btn-group-justified>.btn,.btn-group-justified>.btn-group{display:table-cell;float:none;width:1%}.btn-group-justified>.btn-group .btn{width:100%}.btn-group-justified>.btn-group .dropdown-menu{left:auto}[data-toggle=buttons]>.btn input[type=checkbox],[data-toggle=buttons]>.btn input[type=radio],[data-toggle=buttons]>.btn-group>.btn input[type=checkbox],[data-toggle=buttons]>.btn-group>.btn input[type=radio]{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.input-group{position:relative;display:table;border-collapse:separate}.input-group[class*=col-]{float:none;padding-right:0;padding-left:0}.input-group .form-control{position:relative;z-index:2;float:left;width:100%;margin-bottom:0}.input-group .form-control:focus{z-index:3}.input-group-lg>.form-control,.input-group-lg>.input-group-addon,.input-group-lg>.input-group-btn>.btn{height:46px;padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}select.input-group-lg>.form-control,select.input-group-lg>.input-group-addon,select.input-group-lg>.input-group-btn>.btn{height:46px;line-height:46px}select[multiple].input-group-lg>.form-control,select[multiple].input-group-lg>.input-group-addon,select[multiple].input-group-lg>.input-group-btn>.btn,textarea.input-group-lg>.form-control,textarea.input-group-lg>.input-group-addon,textarea.input-group-lg>.input-group-btn>.btn{height:auto}.input-group-sm>.form-control,.input-group-sm>.input-group-addon,.input-group-sm>.input-group-btn>.btn{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-group-sm>.form-control,select.input-group-sm>.input-group-addon,select.input-group-sm>.input-group-btn>.btn{height:30px;line-height:30px}select[multiple].input-group-sm>.form-control,select[multiple].input-group-sm>.input-group-addon,select[multiple].input-group-sm>.input-group-btn>.btn,textarea.input-group-sm>.form-control,textarea.input-group-sm>.input-group-addon,textarea.input-group-sm>.input-group-btn>.btn{height:auto}.input-group .form-control,.input-group-addon,.input-group-btn{display:table-cell}.input-group .form-control:not(:first-child):not(:last-child),.input-group-addon:not(:first-child):not(:last-child),.input-group-btn:not(:first-child):not(:last-child){border-radius:0}.input-group-addon,.input-group-btn{width:1%;white-space:nowrap;vertical-align:middle}.input-group-addon{padding:6px 12px;font-size:14px;font-weight:400;line-height:1;color:#555;text-align:center;background-color:#eee;border:1px solid #ccc;border-radius:4px}.input-group-addon.input-sm{padding:5px 10px;font-size:12px;border-radius:3px}.input-group-addon.input-lg{padding:10px 16px;font-size:18px;border-radius:6px}.input-group-addon input[type=checkbox],.input-group-addon input[type=radio]{margin-top:0}.input-group .form-control:first-child,.input-group-addon:first-child,.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group>.btn,.input-group-btn:first-child>.dropdown-toggle,.input-group-btn:last-child>.btn-group:not(:last-child)>.btn,.input-group-btn:last-child>.btn:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.input-group-addon:first-child{border-right:0}.input-group .form-control:last-child,.input-group-addon:last-child,.input-group-btn:first-child>.btn-group:not(:first-child)>.btn,.input-group-btn:first-child>.btn:not(:first-child),.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group>.btn,.input-group-btn:last-child>.dropdown-toggle{border-top-left-radius:0;border-bottom-left-radius:0}.input-group-addon:last-child{border-left:0}.input-group-btn{position:relative;font-size:0;white-space:nowrap}.input-group-btn>.btn{position:relative}.input-group-btn>.btn+.btn{margin-left:-1px}.input-group-btn>.btn:active,.input-group-btn>.btn:focus,.input-group-btn>.btn:hover{z-index:2}.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group{margin-right:-1px}.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group{z-index:2;margin-left:-1px}.nav{padding-left:0;margin-bottom:0;list-style:none}.nav>li{position:relative;display:block}.nav>li>a{position:relative;display:block;padding:10px 15px}.nav>li>a:focus,.nav>li>a:hover{text-decoration:none;background-color:#eee}.nav>li.disabled>a{color:#777}.nav>li.disabled>a:focus,.nav>li.disabled>a:hover{color:#777;text-decoration:none;cursor:not-allowed;background-color:transparent}.nav .open>a,.nav .open>a:focus,.nav .open>a:hover{background-color:#eee;border-color:#337ab7}.nav .nav-divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.nav>li>a>img{max-width:none}.nav-tabs{border-bottom:1px solid #ddd}.nav-tabs>li{float:left;margin-bottom:-1px}.nav-tabs>li>a{margin-right:2px;line-height:1.42857143;border:1px solid transparent;border-radius:4px 4px 0 0}.nav-tabs>li>a:hover{border-color:#eee #eee #ddd}.nav-tabs>li.active>a,.nav-tabs>li.active>a:focus,.nav-tabs>li.active>a:hover{color:#555;cursor:default;background-color:#fff;border:1px solid #ddd;border-bottom-color:transparent}.nav-tabs.nav-justified{width:100%;border-bottom:0}.nav-tabs.nav-justified>li{float:none}.nav-tabs.nav-justified>li>a{margin-bottom:5px;text-align:center}.nav-tabs.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media (min-width:768px){.nav-tabs.nav-justified>li{display:table-cell;width:1%}.nav-tabs.nav-justified>li>a{margin-bottom:0}}.nav-tabs.nav-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:focus,.nav-tabs.nav-justified>.active>a:hover{border:1px solid #ddd}@media (min-width:768px){.nav-tabs.nav-justified>li>a{border-bottom:1px solid #ddd;border-radius:4px 4px 0 0}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:focus,.nav-tabs.nav-justified>.active>a:hover{border-bottom-color:#fff}}.nav-pills>li{float:left}.nav-pills>li>a{border-radius:4px}.nav-pills>li+li{margin-left:2px}.nav-pills>li.active>a,.nav-pills>li.active>a:focus,.nav-pills>li.active>a:hover{color:#fff;background-color:#337ab7}.nav-stacked>li{float:none}.nav-stacked>li+li{margin-top:2px;margin-left:0}.nav-justified{width:100%}.nav-justified>li{float:none}.nav-justified>li>a{margin-bottom:5px;text-align:center}.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media (min-width:768px){.nav-justified>li{display:table-cell;width:1%}.nav-justified>li>a{margin-bottom:0}}.nav-tabs-justified{border-bottom:0}.nav-tabs-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:focus,.nav-tabs-justified>.active>a:hover{border:1px solid #ddd}@media (min-width:768px){.nav-tabs-justified>li>a{border-bottom:1px solid #ddd;border-radius:4px 4px 0 0}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:focus,.nav-tabs-justified>.active>a:hover{border-bottom-color:#fff}}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-left-radius:0;border-top-right-radius:0}.navbar{position:relative;min-height:50px;margin-bottom:20px;border:1px solid transparent}@media (min-width:768px){.navbar{border-radius:4px}}@media (min-width:768px){.navbar-header{float:left}}.navbar-collapse{padding-right:15px;padding-left:15px;overflow-x:visible;-webkit-overflow-scrolling:touch;border-top:1px solid transparent;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 0 rgba(255,255,255,.1)}.navbar-collapse.in{overflow-y:auto}@media (min-width:768px){.navbar-collapse{width:auto;border-top:0;-webkit-box-shadow:none;box-shadow:none}.navbar-collapse.collapse{display:block!important;height:auto!important;padding-bottom:0;overflow:visible!important}.navbar-collapse.in{overflow-y:visible}.navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse,.navbar-static-top .navbar-collapse{padding-right:0;padding-left:0}}.navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse{max-height:340px}@media (max-device-width:480px) and (orientation:landscape){.navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse{max-height:200px}}.container-fluid>.navbar-collapse,.container-fluid>.navbar-header,.container>.navbar-collapse,.container>.navbar-header{margin-right:-15px;margin-left:-15px}@media (min-width:768px){.container-fluid>.navbar-collapse,.container-fluid>.navbar-header,.container>.navbar-collapse,.container>.navbar-header{margin-right:0;margin-left:0}}.navbar-static-top{z-index:1000;border-width:0 0 1px}@media (min-width:768px){.navbar-static-top{border-radius:0}}.navbar-fixed-bottom,.navbar-fixed-top{position:fixed;right:0;left:0;z-index:1030}@media (min-width:768px){.navbar-fixed-bottom,.navbar-fixed-top{border-radius:0}}.navbar-fixed-top{top:0;border-width:0 0 1px}.navbar-fixed-bottom{bottom:0;margin-bottom:0;border-width:1px 0 0}.navbar-brand{float:left;height:50px;padding:15px 15px;font-size:18px;line-height:20px}.navbar-brand:focus,.navbar-brand:hover{text-decoration:none}.navbar-brand>img{display:block}@media (min-width:768px){.navbar>.container .navbar-brand,.navbar>.container-fluid .navbar-brand{margin-left:-15px}}.navbar-toggle{position:relative;float:right;padding:9px 10px;margin-top:8px;margin-right:15px;margin-bottom:8px;background-color:transparent;background-image:none;border:1px solid transparent;border-radius:4px}.navbar-toggle:focus{outline:0}.navbar-toggle .icon-bar{display:block;width:22px;height:2px;border-radius:1px}.navbar-toggle .icon-bar+.icon-bar{margin-top:4px}@media (min-width:768px){.navbar-toggle{display:none}}.navbar-nav{margin:7.5px -15px}.navbar-nav>li>a{padding-top:10px;padding-bottom:10px;line-height:20px}@media (max-width:767px){.navbar-nav .open .dropdown-menu{position:static;float:none;width:auto;margin-top:0;background-color:transparent;border:0;-webkit-box-shadow:none;box-shadow:none}.navbar-nav .open .dropdown-menu .dropdown-header,.navbar-nav .open .dropdown-menu>li>a{padding:5px 15px 5px 25px}.navbar-nav .open .dropdown-menu>li>a{line-height:20px}.navbar-nav .open .dropdown-menu>li>a:focus,.navbar-nav .open .dropdown-menu>li>a:hover{background-image:none}}@media (min-width:768px){.navbar-nav{float:left;margin:0}.navbar-nav>li{float:left}.navbar-nav>li>a{padding-top:15px;padding-bottom:15px}}.navbar-form{padding:10px 15px;margin-top:8px;margin-right:-15px;margin-bottom:8px;margin-left:-15px;border-top:1px solid transparent;border-bottom:1px solid transparent;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1)}@media (min-width:768px){.navbar-form .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.navbar-form .form-control{display:inline-block;width:auto;vertical-align:middle}.navbar-form .form-control-static{display:inline-block}.navbar-form .input-group{display:inline-table;vertical-align:middle}.navbar-form .input-group .form-control,.navbar-form .input-group .input-group-addon,.navbar-form .input-group .input-group-btn{width:auto}.navbar-form .input-group>.form-control{width:100%}.navbar-form .control-label{margin-bottom:0;vertical-align:middle}.navbar-form .checkbox,.navbar-form .radio{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.navbar-form .checkbox label,.navbar-form .radio label{padding-left:0}.navbar-form .checkbox input[type=checkbox],.navbar-form .radio input[type=radio]{position:relative;margin-left:0}.navbar-form .has-feedback .form-control-feedback{top:0}}@media (max-width:767px){.navbar-form .form-group{margin-bottom:5px}.navbar-form .form-group:last-child{margin-bottom:0}}@media (min-width:768px){.navbar-form{width:auto;padding-top:0;padding-bottom:0;margin-right:0;margin-left:0;border:0;-webkit-box-shadow:none;box-shadow:none}}.navbar-nav>li>.dropdown-menu{margin-top:0;border-top-left-radius:0;border-top-right-radius:0}.navbar-fixed-bottom .navbar-nav>li>.dropdown-menu{margin-bottom:0;border-top-left-radius:4px;border-top-right-radius:4px;border-bottom-right-radius:0;border-bottom-left-radius:0}.navbar-btn{margin-top:8px;margin-bottom:8px}.navbar-btn.btn-sm{margin-top:10px;margin-bottom:10px}.navbar-btn.btn-xs{margin-top:14px;margin-bottom:14px}.navbar-text{margin-top:15px;margin-bottom:15px}@media (min-width:768px){.navbar-text{float:left;margin-right:15px;margin-left:15px}}@media (min-width:768px){.navbar-left{float:left!important}.navbar-right{float:right!important;margin-right:-15px}.navbar-right~.navbar-right{margin-right:0}}.navbar-default{background-color:#f8f8f8;border-color:#e7e7e7}.navbar-default .navbar-brand{color:#777}.navbar-default .navbar-brand:focus,.navbar-default .navbar-brand:hover{color:#5e5e5e;background-color:transparent}.navbar-default .navbar-text{color:#777}.navbar-default .navbar-nav>li>a{color:#777}.navbar-default .navbar-nav>li>a:focus,.navbar-default .navbar-nav>li>a:hover{color:#333;background-color:transparent}.navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.active>a:focus,.navbar-default .navbar-nav>.active>a:hover{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav>.disabled>a,.navbar-default .navbar-nav>.disabled>a:focus,.navbar-default .navbar-nav>.disabled>a:hover{color:#ccc;background-color:transparent}.navbar-default .navbar-toggle{border-color:#ddd}.navbar-default .navbar-toggle:focus,.navbar-default .navbar-toggle:hover{background-color:#ddd}.navbar-default .navbar-toggle .icon-bar{background-color:#888}.navbar-default .navbar-collapse,.navbar-default .navbar-form{border-color:#e7e7e7}.navbar-default .navbar-nav>.open>a,.navbar-default .navbar-nav>.open>a:focus,.navbar-default .navbar-nav>.open>a:hover{color:#555;background-color:#e7e7e7}@media (max-width:767px){.navbar-default .navbar-nav .open .dropdown-menu>li>a{color:#777}.navbar-default .navbar-nav .open .dropdown-menu>li>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>li>a:hover{color:#333;background-color:transparent}.navbar-default .navbar-nav .open .dropdown-menu>.active>a,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:hover{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:hover{color:#ccc;background-color:transparent}}.navbar-default .navbar-link{color:#777}.navbar-default .navbar-link:hover{color:#333}.navbar-default .btn-link{color:#777}.navbar-default .btn-link:focus,.navbar-default .btn-link:hover{color:#333}.navbar-default .btn-link[disabled]:focus,.navbar-default .btn-link[disabled]:hover,fieldset[disabled] .navbar-default .btn-link:focus,fieldset[disabled] .navbar-default .btn-link:hover{color:#ccc}.navbar-inverse{background-color:#222;border-color:#080808}.navbar-inverse .navbar-brand{color:#9d9d9d}.navbar-inverse .navbar-brand:focus,.navbar-inverse .navbar-brand:hover{color:#fff;background-color:transparent}.navbar-inverse .navbar-text{color:#9d9d9d}.navbar-inverse .navbar-nav>li>a{color:#9d9d9d}.navbar-inverse .navbar-nav>li>a:focus,.navbar-inverse .navbar-nav>li>a:hover{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav>.active>a,.navbar-inverse .navbar-nav>.active>a:focus,.navbar-inverse .navbar-nav>.active>a:hover{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav>.disabled>a,.navbar-inverse .navbar-nav>.disabled>a:focus,.navbar-inverse .navbar-nav>.disabled>a:hover{color:#444;background-color:transparent}.navbar-inverse .navbar-toggle{border-color:#333}.navbar-inverse .navbar-toggle:focus,.navbar-inverse .navbar-toggle:hover{background-color:#333}.navbar-inverse .navbar-toggle .icon-bar{background-color:#fff}.navbar-inverse .navbar-collapse,.navbar-inverse .navbar-form{border-color:#101010}.navbar-inverse .navbar-nav>.open>a,.navbar-inverse .navbar-nav>.open>a:focus,.navbar-inverse .navbar-nav>.open>a:hover{color:#fff;background-color:#080808}@media (max-width:767px){.navbar-inverse .navbar-nav .open .dropdown-menu>.dropdown-header{border-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu .divider{background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a{color:#9d9d9d}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:hover{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:hover{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:hover{color:#444;background-color:transparent}}.navbar-inverse .navbar-link{color:#9d9d9d}.navbar-inverse .navbar-link:hover{color:#fff}.navbar-inverse .btn-link{color:#9d9d9d}.navbar-inverse .btn-link:focus,.navbar-inverse .btn-link:hover{color:#fff}.navbar-inverse .btn-link[disabled]:focus,.navbar-inverse .btn-link[disabled]:hover,fieldset[disabled] .navbar-inverse .btn-link:focus,fieldset[disabled] .navbar-inverse .btn-link:hover{color:#444}.breadcrumb{padding:8px 15px;margin-bottom:20px;list-style:none;background-color:#f5f5f5;border-radius:4px}.breadcrumb>li{display:inline-block}.breadcrumb>li+li:before{padding:0 5px;color:#ccc;content:"/\00a0"}.breadcrumb>.active{color:#777}.pagination{display:inline-block;padding-left:0;margin:20px 0;border-radius:4px}.pagination>li{display:inline}.pagination>li>a,.pagination>li>span{position:relative;float:left;padding:6px 12px;margin-left:-1px;line-height:1.42857143;color:#337ab7;text-decoration:none;background-color:#fff;border:1px solid #ddd}.pagination>li:first-child>a,.pagination>li:first-child>span{margin-left:0;border-top-left-radius:4px;border-bottom-left-radius:4px}.pagination>li:last-child>a,.pagination>li:last-child>span{border-top-right-radius:4px;border-bottom-right-radius:4px}.pagination>li>a:focus,.pagination>li>a:hover,.pagination>li>span:focus,.pagination>li>span:hover{z-index:2;color:#23527c;background-color:#eee;border-color:#ddd}.pagination>.active>a,.pagination>.active>a:focus,.pagination>.active>a:hover,.pagination>.active>span,.pagination>.active>span:focus,.pagination>.active>span:hover{z-index:3;color:#fff;cursor:default;background-color:#337ab7;border-color:#337ab7}.pagination>.disabled>a,.pagination>.disabled>a:focus,.pagination>.disabled>a:hover,.pagination>.disabled>span,.pagination>.disabled>span:focus,.pagination>.disabled>span:hover{color:#777;cursor:not-allowed;background-color:#fff;border-color:#ddd}.pagination-lg>li>a,.pagination-lg>li>span{padding:10px 16px;font-size:18px;line-height:1.3333333}.pagination-lg>li:first-child>a,.pagination-lg>li:first-child>span{border-top-left-radius:6px;border-bottom-left-radius:6px}.pagination-lg>li:last-child>a,.pagination-lg>li:last-child>span{border-top-right-radius:6px;border-bottom-right-radius:6px}.pagination-sm>li>a,.pagination-sm>li>span{padding:5px 10px;font-size:12px;line-height:1.5}.pagination-sm>li:first-child>a,.pagination-sm>li:first-child>span{border-top-left-radius:3px;border-bottom-left-radius:3px}.pagination-sm>li:last-child>a,.pagination-sm>li:last-child>span{border-top-right-radius:3px;border-bottom-right-radius:3px}.pager{padding-left:0;margin:20px 0;text-align:center;list-style:none}.pager li{display:inline}.pager li>a,.pager li>span{display:inline-block;padding:5px 14px;background-color:#fff;border:1px solid #ddd;border-radius:15px}.pager li>a:focus,.pager li>a:hover{text-decoration:none;background-color:#eee}.pager .next>a,.pager .next>span{float:right}.pager .previous>a,.pager .previous>span{float:left}.pager .disabled>a,.pager .disabled>a:focus,.pager .disabled>a:hover,.pager .disabled>span{color:#777;cursor:not-allowed;background-color:#fff}.label{display:inline;padding:.2em .6em .3em;font-size:75%;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25em}a.label:focus,a.label:hover{color:#fff;text-decoration:none;cursor:pointer}.label:empty{display:none}.btn .label{position:relative;top:-1px}.label-default{background-color:#777}.label-default[href]:focus,.label-default[href]:hover{background-color:#5e5e5e}.label-primary{background-color:#337ab7}.label-primary[href]:focus,.label-primary[href]:hover{background-color:#286090}.label-success{background-color:#5cb85c}.label-success[href]:focus,.label-success[href]:hover{background-color:#449d44}.label-info{background-color:#5bc0de}.label-info[href]:focus,.label-info[href]:hover{background-color:#31b0d5}.label-warning{background-color:#f0ad4e}.label-warning[href]:focus,.label-warning[href]:hover{background-color:#ec971f}.label-danger{background-color:#d9534f}.label-danger[href]:focus,.label-danger[href]:hover{background-color:#c9302c}.badge{display:inline-block;min-width:10px;padding:3px 7px;font-size:12px;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:middle;background-color:#777;border-radius:10px}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.btn-group-xs>.btn .badge,.btn-xs .badge{top:0;padding:1px 5px}a.badge:focus,a.badge:hover{color:#fff;text-decoration:none;cursor:pointer}.list-group-item.active>.badge,.nav-pills>.active>a>.badge{color:#337ab7;background-color:#fff}.list-group-item>.badge{float:right}.list-group-item>.badge+.badge{margin-right:5px}.nav-pills>li>a>.badge{margin-left:3px}.jumbotron{padding-top:30px;padding-bottom:30px;margin-bottom:30px;color:inherit;background-color:#eee}.jumbotron .h1,.jumbotron h1{color:inherit}.jumbotron p{margin-bottom:15px;font-size:21px;font-weight:200}.jumbotron>hr{border-top-color:#d5d5d5}.container .jumbotron,.container-fluid .jumbotron{padding-right:15px;padding-left:15px;border-radius:6px}.jumbotron .container{max-width:100%}@media screen and (min-width:768px){.jumbotron{padding-top:48px;padding-bottom:48px}.container .jumbotron,.container-fluid .jumbotron{padding-right:60px;padding-left:60px}.jumbotron .h1,.jumbotron h1{font-size:63px}}.thumbnail{display:block;padding:4px;margin-bottom:20px;line-height:1.42857143;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:border .2s ease-in-out;-o-transition:border .2s ease-in-out;transition:border .2s ease-in-out}.thumbnail a>img,.thumbnail>img{margin-right:auto;margin-left:auto}a.thumbnail.active,a.thumbnail:focus,a.thumbnail:hover{border-color:#337ab7}.thumbnail .caption{padding:9px;color:#333}.alert{padding:15px;margin-bottom:20px;border:1px solid transparent;border-radius:4px}.alert h4{margin-top:0;color:inherit}.alert .alert-link{font-weight:700}.alert>p,.alert>ul{margin-bottom:0}.alert>p+p{margin-top:5px}.alert-dismissable,.alert-dismissible{padding-right:35px}.alert-dismissable .close,.alert-dismissible .close{position:relative;top:-2px;right:-21px;color:inherit}.alert-success{color:#3c763d;background-color:#dff0d8;border-color:#d6e9c6}.alert-success hr{border-top-color:#c9e2b3}.alert-success .alert-link{color:#2b542c}.alert-info{color:#31708f;background-color:#d9edf7;border-color:#bce8f1}.alert-info hr{border-top-color:#a6e1ec}.alert-info .alert-link{color:#245269}.alert-warning{color:#8a6d3b;background-color:#fcf8e3;border-color:#faebcc}.alert-warning hr{border-top-color:#f7e1b5}.alert-warning .alert-link{color:#66512c}.alert-danger{color:#a94442;background-color:#f2dede;border-color:#ebccd1}.alert-danger hr{border-top-color:#e4b9c0}.alert-danger .alert-link{color:#843534}@-webkit-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@-o-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}.progress{height:20px;margin-bottom:20px;overflow:hidden;background-color:#f5f5f5;border-radius:4px;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,.1);box-shadow:inset 0 1px 2px rgba(0,0,0,.1)}.progress-bar{float:left;width:0;height:100%;font-size:12px;line-height:20px;color:#fff;text-align:center;background-color:#337ab7;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,.15);box-shadow:inset 0 -1px 0 rgba(0,0,0,.15);-webkit-transition:width .6s ease;-o-transition:width .6s ease;transition:width .6s ease}.progress-bar-striped,.progress-striped .progress-bar{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);-webkit-background-size:40px 40px;background-size:40px 40px}.progress-bar.active,.progress.active .progress-bar{-webkit-animation:progress-bar-stripes 2s linear infinite;-o-animation:progress-bar-stripes 2s linear infinite;animation:progress-bar-stripes 2s linear infinite}.progress-bar-success{background-color:#5cb85c}.progress-striped .progress-bar-success{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-info{background-color:#5bc0de}.progress-striped .progress-bar-info{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-warning{background-color:#f0ad4e}.progress-striped .progress-bar-warning{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-danger{background-color:#d9534f}.progress-striped .progress-bar-danger{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.media{margin-top:15px}.media:first-child{margin-top:0}.media,.media-body{overflow:hidden;zoom:1}.media-body{width:10000px}.media-object{display:block}.media-object.img-thumbnail{max-width:none}.media-right,.media>.pull-right{padding-left:10px}.media-left,.media>.pull-left{padding-right:10px}.media-body,.media-left,.media-right{display:table-cell;vertical-align:top}.media-middle{vertical-align:middle}.media-bottom{vertical-align:bottom}.media-heading{margin-top:0;margin-bottom:5px}.media-list{padding-left:0;list-style:none}.list-group{padding-left:0;margin-bottom:20px}.list-group-item{position:relative;display:block;padding:10px 15px;margin-bottom:-1px;background-color:#fff;border:1px solid #ddd}.list-group-item:first-child{border-top-left-radius:4px;border-top-right-radius:4px}.list-group-item:last-child{margin-bottom:0;border-bottom-right-radius:4px;border-bottom-left-radius:4px}a.list-group-item,button.list-group-item{color:#555}a.list-group-item .list-group-item-heading,button.list-group-item .list-group-item-heading{color:#333}a.list-group-item:focus,a.list-group-item:hover,button.list-group-item:focus,button.list-group-item:hover{color:#555;text-decoration:none;background-color:#f5f5f5}button.list-group-item{width:100%;text-align:left}.list-group-item.disabled,.list-group-item.disabled:focus,.list-group-item.disabled:hover{color:#777;cursor:not-allowed;background-color:#eee}.list-group-item.disabled .list-group-item-heading,.list-group-item.disabled:focus .list-group-item-heading,.list-group-item.disabled:hover .list-group-item-heading{color:inherit}.list-group-item.disabled .list-group-item-text,.list-group-item.disabled:focus .list-group-item-text,.list-group-item.disabled:hover .list-group-item-text{color:#777}.list-group-item.active,.list-group-item.active:focus,.list-group-item.active:hover{z-index:2;color:#fff;background-color:#337ab7;border-color:#337ab7}.list-group-item.active .list-group-item-heading,.list-group-item.active .list-group-item-heading>.small,.list-group-item.active .list-group-item-heading>small,.list-group-item.active:focus .list-group-item-heading,.list-group-item.active:focus .list-group-item-heading>.small,.list-group-item.active:focus .list-group-item-heading>small,.list-group-item.active:hover .list-group-item-heading,.list-group-item.active:hover .list-group-item-heading>.small,.list-group-item.active:hover .list-group-item-heading>small{color:inherit}.list-group-item.active .list-group-item-text,.list-group-item.active:focus .list-group-item-text,.list-group-item.active:hover .list-group-item-text{color:#c7ddef}.list-group-item-success{color:#3c763d;background-color:#dff0d8}a.list-group-item-success,button.list-group-item-success{color:#3c763d}a.list-group-item-success .list-group-item-heading,button.list-group-item-success .list-group-item-heading{color:inherit}a.list-group-item-success:focus,a.list-group-item-success:hover,button.list-group-item-success:focus,button.list-group-item-success:hover{color:#3c763d;background-color:#d0e9c6}a.list-group-item-success.active,a.list-group-item-success.active:focus,a.list-group-item-success.active:hover,button.list-group-item-success.active,button.list-group-item-success.active:focus,button.list-group-item-success.active:hover{color:#fff;background-color:#3c763d;border-color:#3c763d}.list-group-item-info{color:#31708f;background-color:#d9edf7}a.list-group-item-info,button.list-group-item-info{color:#31708f}a.list-group-item-info .list-group-item-heading,button.list-group-item-info .list-group-item-heading{color:inherit}a.list-group-item-info:focus,a.list-group-item-info:hover,button.list-group-item-info:focus,button.list-group-item-info:hover{color:#31708f;background-color:#c4e3f3}a.list-group-item-info.active,a.list-group-item-info.active:focus,a.list-group-item-info.active:hover,button.list-group-item-info.active,button.list-group-item-info.active:focus,button.list-group-item-info.active:hover{color:#fff;background-color:#31708f;border-color:#31708f}.list-group-item-warning{color:#8a6d3b;background-color:#fcf8e3}a.list-group-item-warning,button.list-group-item-warning{color:#8a6d3b}a.list-group-item-warning .list-group-item-heading,button.list-group-item-warning .list-group-item-heading{color:inherit}a.list-group-item-warning:focus,a.list-group-item-warning:hover,button.list-group-item-warning:focus,button.list-group-item-warning:hover{color:#8a6d3b;background-color:#faf2cc}a.list-group-item-warning.active,a.list-group-item-warning.active:focus,a.list-group-item-warning.active:hover,button.list-group-item-warning.active,button.list-group-item-warning.active:focus,button.list-group-item-warning.active:hover{color:#fff;background-color:#8a6d3b;border-color:#8a6d3b}.list-group-item-danger{color:#a94442;background-color:#f2dede}a.list-group-item-danger,button.list-group-item-danger{color:#a94442}a.list-group-item-danger .list-group-item-heading,button.list-group-item-danger .list-group-item-heading{color:inherit}a.list-group-item-danger:focus,a.list-group-item-danger:hover,button.list-group-item-danger:focus,button.list-group-item-danger:hover{color:#a94442;background-color:#ebcccc}a.list-group-item-danger.active,a.list-group-item-danger.active:focus,a.list-group-item-danger.active:hover,button.list-group-item-danger.active,button.list-group-item-danger.active:focus,button.list-group-item-danger.active:hover{color:#fff;background-color:#a94442;border-color:#a94442}.list-group-item-heading{margin-top:0;margin-bottom:5px}.list-group-item-text{margin-bottom:0;line-height:1.3}.panel{margin-bottom:20px;background-color:#fff;border:1px solid transparent;border-radius:4px;-webkit-box-shadow:0 1px 1px rgba(0,0,0,.05);box-shadow:0 1px 1px rgba(0,0,0,.05)}.panel-body{padding:15px}.panel-heading{padding:10px 15px;border-bottom:1px solid transparent;border-top-left-radius:3px;border-top-right-radius:3px}.panel-heading>.dropdown .dropdown-toggle{color:inherit}.panel-title{margin-top:0;margin-bottom:0;font-size:16px;color:inherit}.panel-title>.small,.panel-title>.small>a,.panel-title>a,.panel-title>small,.panel-title>small>a{color:inherit}.panel-footer{padding:10px 15px;background-color:#f5f5f5;border-top:1px solid #ddd;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.list-group,.panel>.panel-collapse>.list-group{margin-bottom:0}.panel>.list-group .list-group-item,.panel>.panel-collapse>.list-group .list-group-item{border-width:1px 0;border-radius:0}.panel>.list-group:first-child .list-group-item:first-child,.panel>.panel-collapse>.list-group:first-child .list-group-item:first-child{border-top:0;border-top-left-radius:3px;border-top-right-radius:3px}.panel>.list-group:last-child .list-group-item:last-child,.panel>.panel-collapse>.list-group:last-child .list-group-item:last-child{border-bottom:0;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.panel-heading+.panel-collapse>.list-group .list-group-item:first-child{border-top-left-radius:0;border-top-right-radius:0}.panel-heading+.list-group .list-group-item:first-child{border-top-width:0}.list-group+.panel-footer{border-top-width:0}.panel>.panel-collapse>.table,.panel>.table,.panel>.table-responsive>.table{margin-bottom:0}.panel>.panel-collapse>.table caption,.panel>.table caption,.panel>.table-responsive>.table caption{padding-right:15px;padding-left:15px}.panel>.table-responsive:first-child>.table:first-child,.panel>.table:first-child{border-top-left-radius:3px;border-top-right-radius:3px}.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child,.panel>.table:first-child>thead:first-child>tr:first-child{border-top-left-radius:3px;border-top-right-radius:3px}.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:first-child,.panel>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table:first-child>thead:first-child>tr:first-child th:first-child{border-top-left-radius:3px}.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:last-child,.panel>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table:first-child>thead:first-child>tr:first-child th:last-child{border-top-right-radius:3px}.panel>.table-responsive:last-child>.table:last-child,.panel>.table:last-child{border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child{border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:first-child,.panel>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:first-child{border-bottom-left-radius:3px}.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:last-child{border-bottom-right-radius:3px}.panel>.panel-body+.table,.panel>.panel-body+.table-responsive,.panel>.table+.panel-body,.panel>.table-responsive+.panel-body{border-top:1px solid #ddd}.panel>.table>tbody:first-child>tr:first-child td,.panel>.table>tbody:first-child>tr:first-child th{border-top:0}.panel>.table-bordered,.panel>.table-responsive>.table-bordered{border:0}.panel>.table-bordered>tbody>tr>td:first-child,.panel>.table-bordered>tbody>tr>th:first-child,.panel>.table-bordered>tfoot>tr>td:first-child,.panel>.table-bordered>tfoot>tr>th:first-child,.panel>.table-bordered>thead>tr>td:first-child,.panel>.table-bordered>thead>tr>th:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:first-child,.panel>.table-responsive>.table-bordered>thead>tr>td:first-child,.panel>.table-responsive>.table-bordered>thead>tr>th:first-child{border-left:0}.panel>.table-bordered>tbody>tr>td:last-child,.panel>.table-bordered>tbody>tr>th:last-child,.panel>.table-bordered>tfoot>tr>td:last-child,.panel>.table-bordered>tfoot>tr>th:last-child,.panel>.table-bordered>thead>tr>td:last-child,.panel>.table-bordered>thead>tr>th:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:last-child,.panel>.table-responsive>.table-bordered>thead>tr>td:last-child,.panel>.table-responsive>.table-bordered>thead>tr>th:last-child{border-right:0}.panel>.table-bordered>tbody>tr:first-child>td,.panel>.table-bordered>tbody>tr:first-child>th,.panel>.table-bordered>thead>tr:first-child>td,.panel>.table-bordered>thead>tr:first-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>th,.panel>.table-responsive>.table-bordered>thead>tr:first-child>td,.panel>.table-responsive>.table-bordered>thead>tr:first-child>th{border-bottom:0}.panel>.table-bordered>tbody>tr:last-child>td,.panel>.table-bordered>tbody>tr:last-child>th,.panel>.table-bordered>tfoot>tr:last-child>td,.panel>.table-bordered>tfoot>tr:last-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>th,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>td,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>th{border-bottom:0}.panel>.table-responsive{margin-bottom:0;border:0}.panel-group{margin-bottom:20px}.panel-group .panel{margin-bottom:0;border-radius:4px}.panel-group .panel+.panel{margin-top:5px}.panel-group .panel-heading{border-bottom:0}.panel-group .panel-heading+.panel-collapse>.list-group,.panel-group .panel-heading+.panel-collapse>.panel-body{border-top:1px solid #ddd}.panel-group .panel-footer{border-top:0}.panel-group .panel-footer+.panel-collapse .panel-body{border-bottom:1px solid #ddd}.panel-default{border-color:#ddd}.panel-default>.panel-heading{color:#333;background-color:#f5f5f5;border-color:#ddd}.panel-default>.panel-heading+.panel-collapse>.panel-body{border-top-color:#ddd}.panel-default>.panel-heading .badge{color:#f5f5f5;background-color:#333}.panel-default>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#ddd}.panel-primary{border-color:#337ab7}.panel-primary>.panel-heading{color:#fff;background-color:#337ab7;border-color:#337ab7}.panel-primary>.panel-heading+.panel-collapse>.panel-body{border-top-color:#337ab7}.panel-primary>.panel-heading .badge{color:#337ab7;background-color:#fff}.panel-primary>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#337ab7}.panel-success{border-color:#d6e9c6}.panel-success>.panel-heading{color:#3c763d;background-color:#dff0d8;border-color:#d6e9c6}.panel-success>.panel-heading+.panel-collapse>.panel-body{border-top-color:#d6e9c6}.panel-success>.panel-heading .badge{color:#dff0d8;background-color:#3c763d}.panel-success>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#d6e9c6}.panel-info{border-color:#bce8f1}.panel-info>.panel-heading{color:#31708f;background-color:#d9edf7;border-color:#bce8f1}.panel-info>.panel-heading+.panel-collapse>.panel-body{border-top-color:#bce8f1}.panel-info>.panel-heading .badge{color:#d9edf7;background-color:#31708f}.panel-info>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#bce8f1}.panel-warning{border-color:#faebcc}.panel-warning>.panel-heading{color:#8a6d3b;background-color:#fcf8e3;border-color:#faebcc}.panel-warning>.panel-heading+.panel-collapse>.panel-body{border-top-color:#faebcc}.panel-warning>.panel-heading .badge{color:#fcf8e3;background-color:#8a6d3b}.panel-warning>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#faebcc}.panel-danger{border-color:#ebccd1}.panel-danger>.panel-heading{color:#a94442;background-color:#f2dede;border-color:#ebccd1}.panel-danger>.panel-heading+.panel-collapse>.panel-body{border-top-color:#ebccd1}.panel-danger>.panel-heading .badge{color:#f2dede;background-color:#a94442}.panel-danger>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#ebccd1}.embed-responsive{position:relative;display:block;height:0;padding:0;overflow:hidden}.embed-responsive .embed-responsive-item,.embed-responsive embed,.embed-responsive iframe,.embed-responsive object,.embed-responsive video{position:absolute;top:0;bottom:0;left:0;width:100%;height:100%;border:0}.embed-responsive-16by9{padding-bottom:56.25%}.embed-responsive-4by3{padding-bottom:75%}.well{min-height:20px;padding:19px;margin-bottom:20px;background-color:#f5f5f5;border:1px solid #e3e3e3;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.05);box-shadow:inset 0 1px 1px rgba(0,0,0,.05)}.well blockquote{border-color:#ddd;border-color:rgba(0,0,0,.15)}.well-lg{padding:24px;border-radius:6px}.well-sm{padding:9px;border-radius:3px}.close{float:right;font-size:21px;font-weight:700;line-height:1;color:#000;text-shadow:0 1px 0 #fff;filter:alpha(opacity=20);opacity:.2}.close:focus,.close:hover{color:#000;text-decoration:none;cursor:pointer;filter:alpha(opacity=50);opacity:.5}button.close{-webkit-appearance:none;padding:0;cursor:pointer;background:0 0;border:0}.modal-open{overflow:hidden}.modal{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1050;display:none;overflow:hidden;-webkit-overflow-scrolling:touch;outline:0}.modal.fade .modal-dialog{-webkit-transition:-webkit-transform .3s ease-out;-o-transition:-o-transform .3s ease-out;transition:transform .3s ease-out;-webkit-transform:translate(0,-25%);-ms-transform:translate(0,-25%);-o-transform:translate(0,-25%);transform:translate(0,-25%)}.modal.in .modal-dialog{-webkit-transform:translate(0,0);-ms-transform:translate(0,0);-o-transform:translate(0,0);transform:translate(0,0)}.modal-open .modal{overflow-x:hidden;overflow-y:auto}.modal-dialog{position:relative;width:auto;margin:10px}.modal-content{position:relative;background-color:#fff;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid #999;border:1px solid rgba(0,0,0,.2);border-radius:6px;outline:0;-webkit-box-shadow:0 3px 9px rgba(0,0,0,.5);box-shadow:0 3px 9px rgba(0,0,0,.5)}.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;background-color:#000}.modal-backdrop.fade{filter:alpha(opacity=0);opacity:0}.modal-backdrop.in{filter:alpha(opacity=50);opacity:.5}.modal-header{padding:15px;border-bottom:1px solid #e5e5e5}.modal-header .close{margin-top:-2px}.modal-title{margin:0;line-height:1.42857143}.modal-body{position:relative;padding:15px}.modal-footer{padding:15px;text-align:right;border-top:1px solid #e5e5e5}.modal-footer .btn+.btn{margin-bottom:0;margin-left:5px}.modal-footer .btn-group .btn+.btn{margin-left:-1px}.modal-footer .btn-block+.btn-block{margin-left:0}.modal-scrollbar-measure{position:absolute;top:-9999px;width:50px;height:50px;overflow:scroll}@media (min-width:768px){.modal-dialog{width:600px;margin:30px auto}.modal-content{-webkit-box-shadow:0 5px 15px rgba(0,0,0,.5);box-shadow:0 5px 15px rgba(0,0,0,.5)}.modal-sm{width:300px}}@media (min-width:992px){.modal-lg{width:900px}}.tooltip{position:absolute;z-index:1070;display:block;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:12px;font-style:normal;font-weight:400;line-height:1.42857143;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;word-wrap:normal;white-space:normal;filter:alpha(opacity=0);opacity:0;line-break:auto}.tooltip.in{filter:alpha(opacity=90);opacity:.9}.tooltip.top{padding:5px 0;margin-top:-3px}.tooltip.right{padding:0 5px;margin-left:3px}.tooltip.bottom{padding:5px 0;margin-top:3px}.tooltip.left{padding:0 5px;margin-left:-3px}.tooltip-inner{max-width:200px;padding:3px 8px;color:#fff;text-align:center;background-color:#000;border-radius:4px}.tooltip-arrow{position:absolute;width:0;height:0;border-color:transparent;border-style:solid}.tooltip.top .tooltip-arrow{bottom:0;left:50%;margin-left:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.top-left .tooltip-arrow{right:5px;bottom:0;margin-bottom:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.top-right .tooltip-arrow{bottom:0;left:5px;margin-bottom:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.right .tooltip-arrow{top:50%;left:0;margin-top:-5px;border-width:5px 5px 5px 0;border-right-color:#000}.tooltip.left .tooltip-arrow{top:50%;right:0;margin-top:-5px;border-width:5px 0 5px 5px;border-left-color:#000}.tooltip.bottom .tooltip-arrow{top:0;left:50%;margin-left:-5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip.bottom-left .tooltip-arrow{top:0;right:5px;margin-top:-5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip.bottom-right .tooltip-arrow{top:0;left:5px;margin-top:-5px;border-width:0 5px 5px;border-bottom-color:#000}.popover{position:absolute;top:0;left:0;z-index:1060;display:none;max-width:276px;padding:1px;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;font-style:normal;font-weight:400;line-height:1.42857143;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;word-wrap:normal;white-space:normal;background-color:#fff;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid #ccc;border:1px solid rgba(0,0,0,.2);border-radius:6px;-webkit-box-shadow:0 5px 10px rgba(0,0,0,.2);box-shadow:0 5px 10px rgba(0,0,0,.2);line-break:auto}.popover.top{margin-top:-10px}.popover.right{margin-left:10px}.popover.bottom{margin-top:10px}.popover.left{margin-left:-10px}.popover-title{padding:8px 14px;margin:0;font-size:14px;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;border-radius:5px 5px 0 0}.popover-content{padding:9px 14px}.popover>.arrow,.popover>.arrow:after{position:absolute;display:block;width:0;height:0;border-color:transparent;border-style:solid}.popover>.arrow{border-width:11px}.popover>.arrow:after{content:"";border-width:10px}.popover.top>.arrow{bottom:-11px;left:50%;margin-left:-11px;border-top-color:#999;border-top-color:rgba(0,0,0,.25);border-bottom-width:0}.popover.top>.arrow:after{bottom:1px;margin-left:-10px;content:" ";border-top-color:#fff;border-bottom-width:0}.popover.right>.arrow{top:50%;left:-11px;margin-top:-11px;border-right-color:#999;border-right-color:rgba(0,0,0,.25);border-left-width:0}.popover.right>.arrow:after{bottom:-10px;left:1px;content:" ";border-right-color:#fff;border-left-width:0}.popover.bottom>.arrow{top:-11px;left:50%;margin-left:-11px;border-top-width:0;border-bottom-color:#999;border-bottom-color:rgba(0,0,0,.25)}.popover.bottom>.arrow:after{top:1px;margin-left:-10px;content:" ";border-top-width:0;border-bottom-color:#fff}.popover.left>.arrow{top:50%;right:-11px;margin-top:-11px;border-right-width:0;border-left-color:#999;border-left-color:rgba(0,0,0,.25)}.popover.left>.arrow:after{right:1px;bottom:-10px;content:" ";border-right-width:0;border-left-color:#fff}.carousel{position:relative}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner>.item{position:relative;display:none;-webkit-transition:.6s ease-in-out left;-o-transition:.6s ease-in-out left;transition:.6s ease-in-out left}.carousel-inner>.item>a>img,.carousel-inner>.item>img{line-height:1}@media all and (transform-3d),(-webkit-transform-3d){.carousel-inner>.item{-webkit-transition:-webkit-transform .6s ease-in-out;-o-transition:-o-transform .6s ease-in-out;transition:transform .6s ease-in-out;-webkit-backface-visibility:hidden;backface-visibility:hidden;-webkit-perspective:1000px;perspective:1000px}.carousel-inner>.item.active.right,.carousel-inner>.item.next{left:0;-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}.carousel-inner>.item.active.left,.carousel-inner>.item.prev{left:0;-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}.carousel-inner>.item.active,.carousel-inner>.item.next.left,.carousel-inner>.item.prev.right{left:0;-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}}.carousel-inner>.active,.carousel-inner>.next,.carousel-inner>.prev{display:block}.carousel-inner>.active{left:0}.carousel-inner>.next,.carousel-inner>.prev{position:absolute;top:0;width:100%}.carousel-inner>.next{left:100%}.carousel-inner>.prev{left:-100%}.carousel-inner>.next.left,.carousel-inner>.prev.right{left:0}.carousel-inner>.active.left{left:-100%}.carousel-inner>.active.right{left:100%}.carousel-control{position:absolute;top:0;bottom:0;left:0;width:15%;font-size:20px;color:#fff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,.6);background-color:rgba(0,0,0,0);filter:alpha(opacity=50);opacity:.5}.carousel-control.left{background-image:-webkit-linear-gradient(left,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);background-image:-o-linear-gradient(left,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);background-image:-webkit-gradient(linear,left top,right top,from(rgba(0,0,0,.5)),to(rgba(0,0,0,.0001)));background-image:linear-gradient(to right,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#80000000', endColorstr='#00000000', GradientType=1);background-repeat:repeat-x}.carousel-control.right{right:0;left:auto;background-image:-webkit-linear-gradient(left,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);background-image:-o-linear-gradient(left,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);background-image:-webkit-gradient(linear,left top,right top,from(rgba(0,0,0,.0001)),to(rgba(0,0,0,.5)));background-image:linear-gradient(to right,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#80000000', GradientType=1);background-repeat:repeat-x}.carousel-control:focus,.carousel-control:hover{color:#fff;text-decoration:none;filter:alpha(opacity=90);outline:0;opacity:.9}.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next,.carousel-control .icon-prev{position:absolute;top:50%;z-index:5;display:inline-block;margin-top:-10px}.carousel-control .glyphicon-chevron-left,.carousel-control .icon-prev{left:50%;margin-left:-10px}.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next{right:50%;margin-right:-10px}.carousel-control .icon-next,.carousel-control .icon-prev{width:20px;height:20px;font-family:serif;line-height:1}.carousel-control .icon-prev:before{content:'\2039'}.carousel-control .icon-next:before{content:'\203a'}.carousel-indicators{position:absolute;bottom:10px;left:50%;z-index:15;width:60%;padding-left:0;margin-left:-30%;text-align:center;list-style:none}.carousel-indicators li{display:inline-block;width:10px;height:10px;margin:1px;text-indent:-999px;cursor:pointer;background-color:#000\9;background-color:rgba(0,0,0,0);border:1px solid #fff;border-radius:10px}.carousel-indicators .active{width:12px;height:12px;margin:0;background-color:#fff}.carousel-caption{position:absolute;right:15%;bottom:20px;left:15%;z-index:10;padding-top:20px;padding-bottom:20px;color:#fff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,.6)}.carousel-caption .btn{text-shadow:none}@media screen and (min-width:768px){.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next,.carousel-control .icon-prev{width:30px;height:30px;margin-top:-10px;font-size:30px}.carousel-control .glyphicon-chevron-left,.carousel-control .icon-prev{margin-left:-10px}.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next{margin-right:-10px}.carousel-caption{right:20%;left:20%;padding-bottom:30px}.carousel-indicators{bottom:20px}}.btn-group-vertical>.btn-group:after,.btn-group-vertical>.btn-group:before,.btn-toolbar:after,.btn-toolbar:before,.clearfix:after,.clearfix:before,.container-fluid:after,.container-fluid:before,.container:after,.container:before,.dl-horizontal dd:after,.dl-horizontal dd:before,.form-horizontal .form-group:after,.form-horizontal .form-group:before,.modal-footer:after,.modal-footer:before,.modal-header:after,.modal-header:before,.nav:after,.nav:before,.navbar-collapse:after,.navbar-collapse:before,.navbar-header:after,.navbar-header:before,.navbar:after,.navbar:before,.pager:after,.pager:before,.panel-body:after,.panel-body:before,.row:after,.row:before{display:table;content:" "}.btn-group-vertical>.btn-group:after,.btn-toolbar:after,.clearfix:after,.container-fluid:after,.container:after,.dl-horizontal dd:after,.form-horizontal .form-group:after,.modal-footer:after,.modal-header:after,.nav:after,.navbar-collapse:after,.navbar-header:after,.navbar:after,.pager:after,.panel-body:after,.row:after{clear:both}.center-block{display:block;margin-right:auto;margin-left:auto}.pull-right{float:right!important}.pull-left{float:left!important}.hide{display:none!important}.show{display:block!important}.invisible{visibility:hidden}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.hidden{display:none!important}.affix{position:fixed}@-ms-viewport{width:device-width}.visible-lg,.visible-md,.visible-sm,.visible-xs{display:none!important}.visible-lg-block,.visible-lg-inline,.visible-lg-inline-block,.visible-md-block,.visible-md-inline,.visible-md-inline-block,.visible-sm-block,.visible-sm-inline,.visible-sm-inline-block,.visible-xs-block,.visible-xs-inline,.visible-xs-inline-block{display:none!important}@media (max-width:767px){.visible-xs{display:block!important}table.visible-xs{display:table!important}tr.visible-xs{display:table-row!important}td.visible-xs,th.visible-xs{display:table-cell!important}}@media (max-width:767px){.visible-xs-block{display:block!important}}@media (max-width:767px){.visible-xs-inline{display:inline!important}}@media (max-width:767px){.visible-xs-inline-block{display:inline-block!important}}@media (min-width:768px) and (max-width:991px){.visible-sm{display:block!important}table.visible-sm{display:table!important}tr.visible-sm{display:table-row!important}td.visible-sm,th.visible-sm{display:table-cell!important}}@media (min-width:768px) and (max-width:991px){.visible-sm-block{display:block!important}}@media (min-width:768px) and (max-width:991px){.visible-sm-inline{display:inline!important}}@media (min-width:768px) and (max-width:991px){.visible-sm-inline-block{display:inline-block!important}}@media (min-width:992px) and (max-width:1199px){.visible-md{display:block!important}table.visible-md{display:table!important}tr.visible-md{display:table-row!important}td.visible-md,th.visible-md{display:table-cell!important}}@media (min-width:992px) and (max-width:1199px){.visible-md-block{display:block!important}}@media (min-width:992px) and (max-width:1199px){.visible-md-inline{display:inline!important}}@media (min-width:992px) and (max-width:1199px){.visible-md-inline-block{display:inline-block!important}}@media (min-width:1200px){.visible-lg{display:block!important}table.visible-lg{display:table!important}tr.visible-lg{display:table-row!important}td.visible-lg,th.visible-lg{display:table-cell!important}}@media (min-width:1200px){.visible-lg-block{display:block!important}}@media (min-width:1200px){.visible-lg-inline{display:inline!important}}@media (min-width:1200px){.visible-lg-inline-block{display:inline-block!important}}@media (max-width:767px){.hidden-xs{display:none!important}}@media (min-width:768px) and (max-width:991px){.hidden-sm{display:none!important}}@media (min-width:992px) and (max-width:1199px){.hidden-md{display:none!important}}@media (min-width:1200px){.hidden-lg{display:none!important}}.visible-print{display:none!important}@media print{.visible-print{display:block!important}table.visible-print{display:table!important}tr.visible-print{display:table-row!important}td.visible-print,th.visible-print{display:table-cell!important}}.visible-print-block{display:none!important}@media print{.visible-print-block{display:block!important}}.visible-print-inline{display:none!important}@media print{.visible-print-inline{display:inline!important}}.visible-print-inline-block{display:none!important}@media print{.visible-print-inline-block{display:inline-block!important}}@media print{.hidden-print{display:none!important}} /*# sourceMappingURL=bootstrap.min.css.map */.nvd3 .nv-axis{pointer-events:none;opacity:1}.nvd3 .nv-axis path{fill:none;stroke:#000;stroke-opacity:.75;shape-rendering:crispEdges}.nvd3 .nv-axis path.domain{stroke-opacity:.75}.nvd3 .nv-axis.nv-x path.domain{stroke-opacity:0}.nvd3 .nv-axis line{fill:none;stroke:#e5e5e5;shape-rendering:crispEdges}.nvd3 .nv-axis .zero line,.nvd3 .nv-axis line.zero{stroke-opacity:.75}.nvd3 .nv-axis .nv-axisMaxMin text{font-weight:700}.nvd3 .x .nv-axis .nv-axisMaxMin text,.nvd3 .x2 .nv-axis .nv-axisMaxMin text,.nvd3 .x3 .nv-axis .nv-axisMaxMin text{text-anchor:middle}.nvd3 .nv-axis.nv-disabled{opacity:0}.nvd3 .nv-bars rect{fill-opacity:.75;transition:fill-opacity 250ms linear;-moz-transition:fill-opacity 250ms linear;-webkit-transition:fill-opacity 250ms linear}.nvd3 .nv-bars rect.hover{fill-opacity:1}.nvd3 .nv-bars .hover rect{fill:#add8e6}.nvd3 .nv-bars text{fill:rgba(0,0,0,0)}.nvd3 .nv-bars .hover text{fill:rgba(0,0,0,1)}.nvd3 .nv-multibar .nv-groups rect,.nvd3 .nv-multibarHorizontal .nv-groups rect,.nvd3 .nv-discretebar .nv-groups rect{stroke-opacity:0;transition:fill-opacity 250ms linear;-moz-transition:fill-opacity 250ms linear;-webkit-transition:fill-opacity 250ms linear}.nvd3 .nv-multibar .nv-groups rect:hover,.nvd3 .nv-multibarHorizontal .nv-groups rect:hover,.nvd3 .nv-candlestickBar .nv-ticks rect:hover,.nvd3 .nv-discretebar .nv-groups rect:hover{fill-opacity:1}.nvd3 .nv-discretebar .nv-groups text,.nvd3 .nv-multibarHorizontal .nv-groups text{font-weight:700;fill:rgba(0,0,0,1);stroke:rgba(0,0,0,0)}.nvd3 .nv-boxplot circle{fill-opacity:.5}.nvd3 .nv-boxplot circle:hover{fill-opacity:1}.nvd3 .nv-boxplot rect:hover{fill-opacity:1}.nvd3 line.nv-boxplot-median{stroke:#000}.nv-boxplot-tick:hover{stroke-width:2.5px}.nvd3.nv-bullet{font:10px sans-serif}.nvd3.nv-bullet .nv-measure{fill-opacity:.8}.nvd3.nv-bullet .nv-measure:hover{fill-opacity:1}.nvd3.nv-bullet .nv-marker{stroke:#000;stroke-width:2px}.nvd3.nv-bullet .nv-markerTriangle{stroke:#000;fill:#fff;stroke-width:1.5px}.nvd3.nv-bullet .nv-tick line{stroke:#666;stroke-width:.5px}.nvd3.nv-bullet .nv-range.nv-s0{fill:#eee}.nvd3.nv-bullet .nv-range.nv-s1{fill:#ddd}.nvd3.nv-bullet .nv-range.nv-s2{fill:#ccc}.nvd3.nv-bullet .nv-title{font-size:14px;font-weight:700}.nvd3.nv-bullet .nv-subtitle{fill:#999}.nvd3.nv-bullet .nv-range{fill:#bababa;fill-opacity:.4}.nvd3.nv-bullet .nv-range:hover{fill-opacity:.7}.nvd3.nv-candlestickBar .nv-ticks .nv-tick{stroke-width:1px}.nvd3.nv-candlestickBar .nv-ticks .nv-tick.hover{stroke-width:2px}.nvd3.nv-candlestickBar .nv-ticks .nv-tick.positive rect{stroke:#2ca02c;fill:#2ca02c}.nvd3.nv-candlestickBar .nv-ticks .nv-tick.negative rect{stroke:#d62728;fill:#d62728}.with-transitions .nv-candlestickBar .nv-ticks .nv-tick{transition:stroke-width 250ms linear,stroke-opacity 250ms linear;-moz-transition:stroke-width 250ms linear,stroke-opacity 250ms linear;-webkit-transition:stroke-width 250ms linear,stroke-opacity 250ms linear}.nvd3.nv-candlestickBar .nv-ticks line{stroke:#333}.nvd3 .nv-legend .nv-disabled rect{}.nvd3 .nv-check-box .nv-box{fill-opacity:0;stroke-width:2}.nvd3 .nv-check-box .nv-check{fill-opacity:0;stroke-width:4}.nvd3 .nv-series.nv-disabled .nv-check-box .nv-check{fill-opacity:0;stroke-opacity:0}.nvd3 .nv-controlsWrap .nv-legend .nv-check-box .nv-check{opacity:0}.nvd3.nv-linePlusBar .nv-bar rect{fill-opacity:.75}.nvd3.nv-linePlusBar .nv-bar rect:hover{fill-opacity:1}.nvd3 .nv-groups path.nv-line{fill:none}.nvd3 .nv-groups path.nv-area{stroke:none}.nvd3.nv-line .nvd3.nv-scatter .nv-groups .nv-point{fill-opacity:0;stroke-opacity:0}.nvd3.nv-scatter.nv-single-point .nv-groups .nv-point{fill-opacity:.5!important;stroke-opacity:.5!important}.with-transitions .nvd3 .nv-groups .nv-point{transition:stroke-width 250ms linear,stroke-opacity 250ms linear;-moz-transition:stroke-width 250ms linear,stroke-opacity 250ms linear;-webkit-transition:stroke-width 250ms linear,stroke-opacity 250ms linear}.nvd3.nv-scatter .nv-groups .nv-point.hover,.nvd3 .nv-groups .nv-point.hover{stroke-width:7px;fill-opacity:.95!important;stroke-opacity:.95!important}.nvd3 .nv-point-paths path{stroke:#aaa;stroke-opacity:0;fill:#eee;fill-opacity:0}.nvd3 .nv-indexLine{cursor:ew-resize}svg.nvd3-svg{-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-ms-user-select:none;-moz-user-select:none;user-select:none;display:block;width:100%;height:100%}.nvtooltip.with-3d-shadow,.with-3d-shadow .nvtooltip{-moz-box-shadow:0 5px 10px rgba(0,0,0,.2);-webkit-box-shadow:0 5px 10px rgba(0,0,0,.2);box-shadow:0 5px 10px rgba(0,0,0,.2);-webkit-border-radius:5px;-moz-border-radius:5px;border-radius:5px}.nvd3 text{font:400 12px Arial}.nvd3 .title{font:700 14px Arial}.nvd3 .nv-background{fill:#fff;fill-opacity:0}.nvd3.nv-noData{font-size:18px;font-weight:700}.nv-brush .extent{fill-opacity:.125;shape-rendering:crispEdges}.nv-brush .resize path{fill:#eee;stroke:#666}.nvd3 .nv-legend .nv-series{cursor:pointer}.nvd3 .nv-legend .nv-disabled circle{fill-opacity:0}.nvd3 .nv-brush .extent{fill-opacity:0!important}.nvd3 .nv-brushBackground rect{stroke:#000;stroke-width:.4;fill:#fff;fill-opacity:.7}.nvd3.nv-ohlcBar .nv-ticks .nv-tick{stroke-width:1px}.nvd3.nv-ohlcBar .nv-ticks .nv-tick.hover{stroke-width:2px}.nvd3.nv-ohlcBar .nv-ticks .nv-tick.positive{stroke:#2ca02c}.nvd3.nv-ohlcBar .nv-ticks .nv-tick.negative{stroke:#d62728}.nvd3 .background path{fill:none;stroke:#EEE;stroke-opacity:.4;shape-rendering:crispEdges}.nvd3 .foreground path{fill:none;stroke-opacity:.7}.nvd3 .nv-parallelCoordinates-brush .extent{fill:#fff;fill-opacity:.6;stroke:gray;shape-rendering:crispEdges}.nvd3 .nv-parallelCoordinates .hover{fill-opacity:1;stroke-width:3px}.nvd3 .missingValuesline line{fill:none;stroke:#000;stroke-width:1;stroke-opacity:1;stroke-dasharray:5,5}.nvd3.nv-pie path{stroke-opacity:0;transition:fill-opacity 250ms linear,stroke-width 250ms linear,stroke-opacity 250ms linear;-moz-transition:fill-opacity 250ms linear,stroke-width 250ms linear,stroke-opacity 250ms linear;-webkit-transition:fill-opacity 250ms linear,stroke-width 250ms linear,stroke-opacity 250ms linear}.nvd3.nv-pie .nv-pie-title{font-size:24px;fill:rgba(19,196,249,.59)}.nvd3.nv-pie .nv-slice text{stroke:#000;stroke-width:0}.nvd3.nv-pie path{stroke:#fff;stroke-width:1px;stroke-opacity:1}.nvd3.nv-pie .hover path{fill-opacity:.7}.nvd3.nv-pie .nv-label{pointer-events:none}.nvd3.nv-pie .nv-label rect{fill-opacity:0;stroke-opacity:0}.nvd3 .nv-groups .nv-point.hover{stroke-width:20px;stroke-opacity:.5}.nvd3 .nv-scatter .nv-point.hover{fill-opacity:1}.nv-noninteractive{pointer-events:none}.nv-distx,.nv-disty{pointer-events:none}.nvd3.nv-sparkline path{fill:none}.nvd3.nv-sparklineplus g.nv-hoverValue{pointer-events:none}.nvd3.nv-sparklineplus .nv-hoverValue line{stroke:#333;stroke-width:1.5px}.nvd3.nv-sparklineplus,.nvd3.nv-sparklineplus g{pointer-events:all}.nvd3 .nv-hoverArea{fill-opacity:0;stroke-opacity:0}.nvd3.nv-sparklineplus .nv-xValue,.nvd3.nv-sparklineplus .nv-yValue{stroke-width:0;font-size:.9em;font-weight:400}.nvd3.nv-sparklineplus .nv-yValue{stroke:#f66}.nvd3.nv-sparklineplus .nv-maxValue{stroke:#2ca02c;fill:#2ca02c}.nvd3.nv-sparklineplus .nv-minValue{stroke:#d62728;fill:#d62728}.nvd3.nv-sparklineplus .nv-currentValue{font-weight:700;font-size:1.1em}.nvd3.nv-stackedarea path.nv-area{fill-opacity:.7;stroke-opacity:0;transition:fill-opacity 250ms linear,stroke-opacity 250ms linear;-moz-transition:fill-opacity 250ms linear,stroke-opacity 250ms linear;-webkit-transition:fill-opacity 250ms linear,stroke-opacity 250ms linear}.nvd3.nv-stackedarea path.nv-area.hover{fill-opacity:.9}.nvd3.nv-stackedarea .nv-groups .nv-point{stroke-opacity:0;fill-opacity:0}.nvtooltip{position:absolute;background-color:rgba(255,255,255,1);color:rgba(0,0,0,1);padding:1px;border:1px solid rgba(0,0,0,.2);z-index:10000;display:block;font-family:Arial;font-size:13px;text-align:left;pointer-events:none;white-space:nowrap;-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.nvtooltip{background:rgba(255,255,255,.8);border:1px solid rgba(0,0,0,.5);border-radius:4px}.nvtooltip.with-transitions,.with-transitions .nvtooltip{transition:opacity 50ms linear;-moz-transition:opacity 50ms linear;-webkit-transition:opacity 50ms linear;transition-delay:200ms;-moz-transition-delay:200ms;-webkit-transition-delay:200ms}.nvtooltip.x-nvtooltip,.nvtooltip.y-nvtooltip{padding:8px}.nvtooltip h3{margin:0;padding:4px 14px;line-height:18px;font-weight:400;background-color:rgba(247,247,247,.75);color:rgba(0,0,0,1);text-align:center;border-bottom:1px solid #ebebeb;-webkit-border-radius:5px 5px 0 0;-moz-border-radius:5px 5px 0 0;border-radius:5px 5px 0 0}.nvtooltip p{margin:0;padding:5px 14px;text-align:center}.nvtooltip span{display:inline-block;margin:2px 0}.nvtooltip table{margin:6px;border-spacing:0}.nvtooltip table td{padding:2px 9px 2px 0;vertical-align:middle}.nvtooltip table td.key{font-weight:400}.nvtooltip table td.value{text-align:right;font-weight:700}.nvtooltip table tr.highlight td{padding:1px 9px 1px 0;border-bottom-style:solid;border-bottom-width:1px;border-top-style:solid;border-top-width:1px}.nvtooltip table td.legend-color-guide div{width:8px;height:8px;vertical-align:middle}.nvtooltip table td.legend-color-guide div{width:12px;height:12px;border:1px solid #999}.nvtooltip .footer{padding:3px;text-align:center}.nvtooltip-pending-removal{pointer-events:none;display:none}.nvd3 .nv-interactiveGuideLine{pointer-events:none}.nvd3 line.nv-guideline{stroke:#ccc}body { padding-top: 10px; } .popover { max-width: none; } .glyphicon { margin-right:.25em; } .table-bordered>thead>tr>td { border-bottom-width: 1px; } .table tbody>tr>td, .table thead>tr>td { padding-top: 3px; padding-bottom: 3px; } .table-condensed tbody>tr>td { padding-top: 0; padding-bottom: 0; } .table .progress { margin-bottom: inherit; } .table-borderless th, .table-borderless td { border: 0 !important; } .table tbody tr.covered-by-large-tests, li.covered-by-large-tests, tr.success, td.success, li.success, span.success { background-color: #dff0d8; } .table tbody tr.covered-by-medium-tests, li.covered-by-medium-tests { background-color: #c3e3b5; } .table tbody tr.covered-by-small-tests, li.covered-by-small-tests { background-color: #99cb84; } .table tbody tr.danger, .table tbody td.danger, li.danger, span.danger { background-color: #f2dede; } .table tbody td.warning, li.warning, span.warning { background-color: #fcf8e3; } .table tbody td.info { background-color: #d9edf7; } td.big { width: 117px; } td.small { } td.codeLine { font-family: monospace; white-space: pre; } td span.comment { color: #888a85; } td span.default { color: #2e3436; } td span.html { color: #888a85; } td span.keyword { color: #2e3436; font-weight: bold; } pre span.string { color: #2e3436; } span.success, span.warning, span.danger { margin-right: 2px; padding-left: 10px; padding-right: 10px; text-align: center; } #classCoverageDistribution, #classComplexity { height: 200px; width: 475px; } #toplink { position: fixed; left: 5px; bottom: 5px; outline: 0; } svg text { font-family: "Lucida Grande", "Lucida Sans Unicode", Verdana, Arial, Helvetica, sans-serif; font-size: 11px; color: #666; fill: #666; } .scrollbox { height:245px; overflow-x:hidden; overflow-y:scroll; } Dashboard for {{full_path}}

    Classes

    Coverage Distribution

    Complexity

    Insufficient Coverage

    {{insufficient_coverage_classes}}
    Class Coverage

    Project Risks

    {{project_risks_classes}}
    Class CRAP

    Methods

    Coverage Distribution

    Complexity

    Insufficient Coverage

    {{insufficient_coverage_methods}}
    Method Coverage

    Project Risks

    {{project_risks_methods}}
    Method CRAP
    Code Coverage for {{full_path}}
    {{items}}
     
    Code Coverage
     
    Lines
    Functions and Methods
    Classes and Traits

    Legend

    Low: 0% to {{low_upper_bound}}% Medium: {{low_upper_bound}}% to {{high_lower_bound}}% High: {{high_lower_bound}}% to 100%

    Generated by php-code-coverage {{version}} using {{runtime}}{{generator}} at {{date}}.

    {{icon}}{{name}} {{lines_bar}}
    {{lines_executed_percent}}
    {{lines_number}}
    {{methods_bar}}
    {{methods_tested_percent}}
    {{methods_number}}
    {{classes_bar}}
    {{classes_tested_percent}}
    {{classes_number}}
    Code Coverage for {{full_path}}
    {{items}}
     
    Code Coverage
     
    Classes and Traits
    Functions and Methods
    Lines
    {{lines}}
    {{name}} {{classes_bar}}
    {{classes_tested_percent}}
    {{classes_number}}
    {{methods_bar}}
    {{methods_tested_percent}}
    {{methods_number}}
    {{crap}} {{lines_bar}}
    {{lines_executed_percent}}
    {{lines_number}}
    NAMLP',(GLYPHICONS HalflingsRegularxVersion 1.009;PS 001.009;hotconv 1.0.70;makeotf.lib2.5.583298GLYPHICONS Halflings RegularBSGPMMF٣(uʌ<0DB/X N CC^ rmR2skPJ"5+glW*iW/E4#ԣU~fUDĹJ1/!/s7k(hN8od$yq19@-HGS"Fjؠ6C3&W51BaQaRU/{*=@dh$1Tۗnc+cA Zɀ@Qcal2>Km' CHMĬfBX,Ype U*Ҕz miO1nE. hx!aC XTV‹ R%|IHP5"bN=r/_R_ %҄uzҘ52ġP)F7SqF{nia@Ds;}9⬥?ź R{Tk;޵ǜU\NZQ-^s7f 0S3A _n`W7Ppi!g/_pZ-=ץ~WZ#/4 KF` z0| Dѵ&däIÏ;M{'omm I !wi9|H:ۧ{~qO, L]&J09/9&Y 蓰{;'3`e@vHyDZ$3Dx28 W Cx5xwB`$C$'ElyhԀ DJ $(pQAA܉A@'$ hp0V0 `se$4$"t2=f4A{Tk0|rH`L&sh]A<`R'!1N;_t3# V *veF`E O${)W=p:F`22ړC^.ćG<.pNe2ִ+Ysl:˼ ܫu5tu^86ȄTmyQ%u~%~1rҘawߚ^_ZZa0!N`. uqYB\ᨀ[e:@J'Eہ,3ubj@pfeW9( ޅ=lG7gj SM609OˑlBa݁ <Bՙ(VRApf^+g9qMt]تpEr@]@VkV ud^X R@?EY2]#Ǽ4JK'dPC|mmn#$+48u'e&[n[L%{BCDL:^!bƙ:&g3-3ub iLZڂWFSId6.k5Pl77UzT:NN.")['|U"AIvwptdk9嫫9nDmq7I|6Kbc]MBABȪ_JT q  6@Fhd`GT:M7'L,IhFP ~j $¡„ 3hA-S^چ-%qe~Qqln"i&Qe?FlK"As(3Y;"Let'RzM1 0{=) K%$C 9M4c EotjVGD)l8,\w !%$3t TBzҴ iUJ[xgdBr$!eq"J> )\~3(^ R€8#>bHG'7_ fӫcκtDoAA߃(qB<``VΫ֘*buP4v@+.Qԥ$V@C0 RܐP[z:XH#e s>?EWO>@I$|si ES)0A?9ab,@K̩o&Q% ϞLu+ +H|Ɛ?NK4CnPt 'OT.j5Ĵ8vw֜I&+`yScaO[#gQd[KI矗`ČLP # )27aTi@c\ސ 0nCpߖ運4͵x*RzYbT[\kUvHʈqp঄IIŗ) bB XPNtz 2 I== ;}bqjiކa#" >11Ap1POOuxQ Fϲ(h݄O'MDxLK$ȵh& 14SirHJPtDM;rM+ *ؗ5u2$f3K %ѳb (@,2f,~"7R;E;HX(42Z'Tۿ2J+^!#oY~4-׃GW*!A0&8f{`W=DP8'= R g}iP>#4EBRY^4eN8V,[BĨD#X],LBsNC> +o^x jC.4Ya_{eA2=r+9POA!! }YPJeGn%x1/}RgHa ^3- 5 |qSaWK{ 1al`I1 Qf_yyCZ)L3X] W6@DMT<.uGK8DsбWr\7Z\V"ISd>CUjeD 3MtWcPӉ6#3QnቩJ\7#磱`؀K lV6 &T ~l. @61`! ` wYk/a0A¹ԁYhdxk:fEao̟^<IwYgq7s[ -y1ع5aMKאRBYFq}8*Nt'.YbZvK (]&ɜ(ՙ2:0 oΏхPKiBH4UX,[$ 0mXش f50VR 8%ާDtUs`-BPzPsvI8z-t1DiB "˶YTJ .?07jLN[2tĮ̎ #6?E׻:ɞY;A&qSIR)ss 9*x0Bj)mHAhyЏhMm&4Ŋ4 gV&tYOCS0Yd7MvNj)wA(o "͢[ E`7ezď-Q]6+Bca@^I:һ=sSnc 6 OB4LGpBq/>O pwj A*@JC[h&3B Qbϩ8 :%f~v/lS00a"B8(f uGoǚgyt_y~͔ %mL !I$Xt0~ePz]Ug Н=_?.j#+`li BM5 őGp7a ֒%Y[UG9@\bDY{{ED0 $Q+FvC`ݨ3Q E\uC9![$l 6DoDgG*+X!%#Cq ?8ZUB)U@opgީZq89|uccAќW;@">Ph_9}.6V/O:3}ZS {:~ykcO6;OB=bV. Rk o ^GV= }oI"+ ]wFzϷ`<30h3]Rf859s`KM8 XUq<\ZOssM&j&  .%PBL~^Gˈ3pD:Z<\ǠiW̆"(:zX~0PG]8RQMNTqfW~!0R%Ց0xvGFy/F-wu/*+ \8@6c<L;c[º nr QS'oQuT{qҐ_ͿSdA*ð:m8Yuz2PB Hh`lkpLLh cEb6eۏҋ ?!>| *=VK@rx0G`%ryr[6Y37 f**n%9df11ޢځ^'] Rq.,^%l e#wWs56!=!q[ %Ԯ]5^:m5)?V b|u7fw,:Ye R% [ o gFAzFPx{dxíw8ٔ{{L> d2CLL,L,(mS$=|%֝lu& ą83 N Xx \VnJ[)Iw/鹻 |GźYDH*Sp60cJ2@W%Ѧc_^$#*:G6n>D;~`9hXB UJB_вˈ%w'$v|#T<68KMϑ-5U+'B ĪNbJOv'|+*Mk(d }C˱@q&aR%} !VЃs3w2a2awHz/Q0F ]~;ä NDP mK3xke_ S!V&=v_PL9؃Yi NU_)J69f*S  17F|BR$y,Ʊ.&=uqsODBR=ɳeؽɇBH 2lu'h7^#S)Xi2..Pe/@FK$](%|2Y1pC8tI11N//+\pjdWmI=߽YZxMЉP81/ JG^U ,Pd1O^ypql2h$jvI%]V .'[+WU8[D,߻-=[ O wE)3J&dقݶR¡S\. 5J$I&oHȳ~ lz> Ux/Hu;?Gt{?;TH L|F8}{p:2t͆aѧp65Y"LD.rVS_ k]n&Hz~9æ p $4ق'{&M\ΰч!qi (.h' B T|{I6cL.빍iI꫿\!;g`1 j%C o3*60E؎]t.-%0 YK_nft] *VFCtJT+\WZ8gF^ ޞf 5I=#6.@2z;W`B/ęQghjyJNAX3, K66ڲM0T@O{4kj|"ftџۄU<-a5b)^R8:ilKa6@!]buvΏ$ oUœ~:.Lte JξP l$S[z~Rq39钺9Q/m"%ʤ7 5MKL鑧"IߏG XTގXLFݧV jp^/Mgۻ{w *9Oʈ<"aAq.M2@mp^'wߕmkxO8 $[&|YZy`2_|%r/J?QṈl3ÞKE$wvCh a@U1M%0?1* $GZ{!|ʿ$ە-٪Ev;͓:`Bl˸쌧ɬoQ0&,F?^s,ch˕$Ecl0w`⏺ň@/r^l8cT3k@JݔuP&ʪNdJjTKi *uX{tj~ɡ}i\BKenȵ|N u#]@lCZ$iPa㸩t04y20 s֪,Au!QBϖ^@Vsɑ\Za7쾉ш6-TrU u~1HJ(<αbRԖqi J?eG *jVħ ":Y);-Fd!HG~ux cb6m)&;0dU?8X~12ۼtIx5{(z '[ŃkZЅi,b1̇`(mHNeK/ [(#QGduT^m%!(7KgP=hϕkɐU+.[eC"GDΨ<*Ř 9&܂?)\<&Ŏ5 LJu@Y,냲ھ_w0^17p޻*>D8_)$UźR!jOF>{ t,-bP,m`D"/zA ͔إQZG&U]xejxLwv~=)@B6?!;53/ps@tOZS7ؙnlxZ?Zj a{6L41 2Qi&֥l]o=7ļ ofЖr MEV@H/aD٦HlK5)ŒZ OE3IG'г;D'zl(E$.ٜ-W R'\w+)w3꺾 @%R).~9;].šg+)%ȝk҉^NW>b1z:soD K2w[|>9vWMFu`axchիU`*ʆe]OV'6xd?H]_rA+zdFH ʋ<ǴkUsFzaH9-gvb=L/E).x9j%B)$AB t b.bAEZRbH(Jya9Wj0fF'Xz $DQ6q` o i={#4FYH@J3 3i~tYТhkHP17YD"pĦ;'16fpu>FoDQin̒- @P# hj ނŀfC 7°T5HVXpklĭ]yXr)?ͺBNJ B#9e&&_0=pZ6h) ̗a b=(p);.N,W^ *hԺCm}E7i6aIvͲxp*Ac#4N&`)ĉHWey7jloEh_n3 jp?4p2WE'kT_ &!ȖjVlHӻ_kɚʳaY s@[G"bYLܫXi Cq8&zVaY{#I@2m!d[1 AƢnKeם/>dmuX:xʷ\pNl+H+ctSǶC[~3e}6 \,Ʉ|Yݧv]'|&M2 ddsx-((76aXm=ӊQ<$Q†\ qiH阇i'i$"{S*VwF/tfQCWUZ{S;Nx}H&* 9׸qU1 a`(M-aG}n̽0 pmcn ɘ_\l} 9FvHþkJZNO mZQҤ aSf )QC+2 d[ H"t* c*bڢq,#S#u'Ҭ:4asCDMF|ɸm_1L]Y\*X>tgDd@&[)8;<{8<+VG\H^aae-4sJA \ hM[\`#pD5Z97g;BWmqTXX%0v&]E 4]FIJ&S_4R0D+meY gO+M{03v'ͅft:;ر Nn\ǔ^,)1laBZZ[  ZSUYh߆wS\/*?zQЋ`X4gr[CWG.Y0Q|RԃE[wy),ш$NK@c/b -#ZI G$ƗtmH#)XwPZAD|S ofTH)>M1b 7ɆSuq jK4[s xL Ǣ]5 !M!AdƧN><:ǻZ(8)e /W| bDDŃtT7rur0Ң`ܴh5 5S}4hrvalc!ZjB]xDbTxzYS6_)op>#@PS*bS\q ƋxYfQ><" Y6IEr_7Ұ VH!IrEL6!Nq"'daqMvA% v n.;A/2ʲa8D$GWv#̏ 9k'o؟o@ (]gk+}/ (nqK(f Ɵиp23YwpDdGq2$}KӯA"E&Ntg'Nes!Ю4qo}쿝S,ojr/s TMT&Qf\12h'&ctN'Tx7]2 ;G ʅ|T++:%/ 1T  ˀ<4͔˗ ,0~!WO' :suҦن(^ﮎ )7fmlҹ1ūtZh L0 6X"J҂ 49 ֩B}ԭ``Ӓ #Jn_F H|$OK=œi17o-Hqp[ɫ%%:Ɉi3۠G CLL4S:dBj|pYSDP>pv5KLe{t0yEND$*;z5NBIgn.N|׶nRaSZJcH mXek;_ 6,yb0#ZA e|wG U1lLD7ÄVqt[xuEQULPBlZSh.1Q0Uٱ8Rip;{H#GON!?t>Q |pkq!gT,j2sǍ4툊tjnƛ/IOE!ˋnF4M&1x$ew+vS  bm]e%8 P !s_06)Q2JB [t9'Ԝ,[fÆג]BB@r&Bs|Q gOC1J D&LJiC`A^#X8tH?daĖTSTaH0@U)^e}Jb7%ܔ%:ƿ@M+ysqL Y00ÔGD >ĩAW 2I:F 32ʠq:6S]K"g[ ϑHB5VEqLJX{CB!PIq9Llxʪ7>֤]@!@9H!pə$ ?)܎l/"́+@`}}:\ 8zQgS+򒤿C}R:HUF\Xg/AZ%c1wlETwX ZNhyf2D ø&vLq47z\iJyJ-kN3 -sJ5)V0N0d\ӛd0d-E[mf\UmxCR<(`ѕp4^!hQ `!l ~ƙ:JɠlW9˸ZXB=l)`jeVJUG!s1?Ƽ3Ê.}bIa6ʕ t?SxZJ'p i,.R2T`5-R BxrWH JPe#Bb|-[PEh‹(5Sfr/]IƊ dE#OS39ӻ]eۮɹ.9_beM9b#e(- 0Ra9"U,%~X܀z۽{'6[@t[W%* .d'vR {h!AedCE}x=E[|B$7J* B- ,=k7[_-I J5e̶{ ( ;WMw`~pAz 8f))(@ Īم<.a%N n@bz>%T*?lgbd<ĵw9Na8;<^*%y:tDҕZ<@0q4l\ 1`/$IJ ғsN);:A;)$ו Wwy%KrIv\bV\nd{6tv/~*O 7U>8rAC<jE-j牷xs)D1Ì/qp**̸$ّ,  Bȼpk MhpK7U]h&-$鎻Y;q6wzW˄֭AhD^R"s5fw +Q&/9ȂwNbz{Y> ]NEc,ߞ# BF:0/-EȾŒ׃F\I{tAZCORuk i)ytkdN&vA P{P'>xƆ`.%,;:Կ:aFoTQ}v#ףQk's~z5hMQʒY>CʍiUNF#J0uC8k! fv {E/IKIE> pyde ʾ=z:@7J|5g8x 3O 3H1؄F.yfzWIM j[.w%i?҆Uf|}@+[8k7CxSEOޯp$Q+:<]K3T-y[Nz;y-HZY^.M*'h8A.N2rLB 7:Or}CS˚S9Jq#WI}*8D!# g#Y>8` В ?a2H,^'?^nhOƒi<Ya2+6aFaMG-Gkè1TbL `*ـVX *xe§֊Z*c`VSbJU*6TK@zqPhg*ߔU(QU49L cM*TR!R,BȅE*C|TzpF@4*텰جXbL.T2y`Upb T,%@` #?@tGLŞS)ÿztϲFy׎ 14Lhfe(.)pK@\ Xe@TbvhD&0-IbD d@ZD1@ DyѧCN| 94Ӛ#Nc l;, `cX@(2$0 "@- $B@<$А8p7C b(@ PA@F 0tGORIJITySMW52\ToRKV0Ȏ( -$ !6wHGO r~e~/]V~/P~7SzKFv`;`9v# JBN,ӭ'`'`\LTApBs)r! ( i` pFFTMm*GDEFD OS/2gk8`cmapڭrcvt ( gaspglyf}]oheadM/6hhea D$hmtx `tlocao0maxpj name,post5 webfTPT=vuvs Z 2UKWN@ { , h, h@( + / _ "#%&&' ' )9IY`iy )9FIYiy !'9IY` * / _ "#%&&' ' 0@P`bp 0@HP`p !#0@P`fbߵiY!     |vpjdc]WQKED 5 *+  / / _ _  ""##%%&&&&' ' '' !& )009:@IDPYN``XbiYpyaku } )09@FHIPY`ipy !!#'09@IPY `` ((h ./<2<2/<2<23!%3#(@ (ddLL[27>32+&/#"&/.=/&6?#"&'&546?>;'.?654676X& j  j )"& j  j )L j )"& j  j )"& j LL#32!2#!+"&5!"&=463!46^^L^^p@LE32!2+!2++"&=!"&?>;5!"&?>;&'&6;22?69  x } x }  x } x v L   d    d  l d;2#4.#"!!!!32>53#"'.'#7367#73>76p<#4@9+820{dd 09B49@4#bkv$B dpd>uhi-K0! .O2d22dJtB+"0J+ku0wd/5dW%{L>G!2+!2++"&=!"&?>;5!"&?>;4632654&#^CjB0  0BjC x  x u x u@--@$?2O*$ $*P2@%d    d   BVT@L!2#!"&=46 %A+32!546;5467.=#"&=!54&'.467>=2cQQc22cQQc2A7 7AA7 7Ad[##[[##[dd76!' Pԇ $ op zy#%**%$ pdL #7!2"'&6&546 6'&4#!"&7622?62~   \l lL 7  &   l 2'7' & c_"fn &\`tfjpO32!546;! 22&&L%6.676.67646p'0SFO$WOHBXAO$WOHB"7Q)mr *`)nq&* )2"'#'".4>"2>4&ȶNN;)wdNNrVVVVNdy%:MNȶ[VVVdXD>.54>0{xuX6Cy>>xC8ZvxyDH-Sv@9yUUy9@vS-H^{62!2'%&7%&63 a o  ^{"62!2'%&7%&63#7'7#'JJN a o  d⋌&2##!"&=467%>="&=46X|>& f   f &>|.hK  ]  ]  Kh.| L#'+/37GKOSW!2#!"&54635)"3!2654&33535!3535!35!"3!2654&35!3535!35~  Ud  & sdd dd d  & d dd dL   ddd  ^ ddddddddddd  ^ dddddddddLL/?!2#!"&546)2#!"&546!2#!"&546)2#!"&5462pmppmpLpppp LL/?O_o32+"&=46!32+"&=46!32+"&=4632+"&=46!32+"&=46!32+"&=4632+"&=46!32+"&=46!32+"&=462LppL/?O_32+"&=46)2#!"&=4632+"&=46)2#!"&=4632+"&=46)2#!"&=462DDDLpp&,  62"'&4?622;;nnBB# "' "/&47 &4?62 62    ;    %I2"'#".4>"2>4&3232++"&=#"&=46;546ijMN,mwbMMoXXXX K  K K  KMbyl+MMijMXXX# K K  K K %52"'#".4>"2>4&!2#!"&=46ijMN,mwbMMoXXXXX^  Mbyl+MMijMXXX  -32+"&5465".5472>54&&dd[֛[ҧg|rr|p>ٸu֛[[u'>7xtrrtxd/?32+"&54632+"&54632+"&54632+"&=46  ޖ  ޖ  ޖ    ~ p     >     GO27'#"/&/&'7'&/&54?6?'6776?6"264X!)&1-=+PP08,2&+!)&1-<,P  P/:-1&+x~~~P09,1&+"(&1,=,QQ09-0&* !(&0-=,P~~~d!%)-1!2!2!5463!546!5#!"&53333333,);  ;),,;)D);dddddddd;)d KK d);ddd);;) dDDDD 62++"&5!+"&5#"&l`    j`  w  ? d3!#!"&5463#"&=X;),Rp);vLp02".4>"2>4&3232+"&546֛[[֛[[rrrr|2   [֛[[֛;rrr   2  ^  )#!3333))p,p,d/3232"'&6;4632#!"&546;2!546& & T2   2 >p  ^  12".4>"2>4&3232"'&6;46֛[[֛[[rrrr|  & [֛[[֛;rrr   12".4>"2>4&%++"&5#"&762֛[[֛[[rrrr   &[֛[[֛;rrr  9!2#!"&'&547>!";2;26?>;26'.    W & & W tW    >     '2".4>"2>4&&546֛[[֛[[rrrr[֛[[֛;rrr] $  (76#!"&?&#"2>53".4>32  mtrrr[֛[[u$  Lrrrtu֛[[֛[576#!"&?&#"#4>323#"'&5463!232>  ntr[u[u  h ntr$  Krtu֛[u֛[v h  Lr d/?O_o!2#!"&546!"3!2654&32+"&=463!2#!"&=4632+"&=463!2#!"&=4632+"&=463!2#!"&=4632+"&=463!2#!"&=46}    R 2  2   > 2  2   > 2  2   > 2  2   >   ~   R d 2  2  2  2  2  2  2  2  2  2  2  2  2  2  2  2 L#54&#!"#"3!2654&#!546;2uSRvd);;));;) SuvR;));;)X);dLL 732#462#".'.#"#"'&5>763276}2 d!C@1?*'),GUKx;(.9)-EgPL 3 0[;P$ 97W W!1A2+"&54. +"&54>32+"&546!32+"&546ޣc 2  2 c*  `  ct  ,rr  ,tޣ 4  4  G9%6'%&+"&546;2762"/"/&4?'&4?62A   Xx"xx"xx"ww".   ^ x"xx"ww"xx"r/%6'%&+"&546;2%3"/.7654'&6?6A    `Z  HN.   ^ d  g~jb1K3#"/.7654&'&6?6%6'%&+"&546;2%3"/.7654'&6?6 D@  *o;7 *    `Z  HN iT "ZG !   ^ d  g~j  !%-;?CGKO3#!#!#3!##5!!!!#53#533!3533##5#535#5!!#53#53#53!5!ddpddX,,ddddD dddd,D,ddddd dd,dddX d,,d,,ddd dddddd,dddddd  #7#3#3#3#3#3!5!#53#53#53ddddddd,,dddd,Pdd[[[[[   "'463&"260V C;S;;S;V0 ;;T;;  ! "'463!"/ &"260V 08D;S;;S;V0 V08;;T;;d&!2&54&#!"3!2#!"&54?6,9K@  D@   K|@  @  J  L !2 46 >>CEU!"3!26?6'.#"#!"&/.+";26=463!2;2654&!"3!26/.6D N9  >SV N N      & X & l l- p  v       dL!)13232#!"&546;>35"264$2"&48]4$);;));;) '3]dϾV<?!(% _5,Ry:" *28 T2*BBW-ޑY". BB % Zd'2;#!5>54.'52%32654.+32654&+50;*7Xml0 ); !9uc>--Ni*S>vPR}^3:R.CuN7Y3(;  G)IsC3[:+ 1aJ);4ePZo!56764.'&'5mSB ,J   95(1(aaR@ 9%/#4.+!52>5#"#!#3'3#72 &2p"& 2KK}}KK} dd R ,১ !%/#4.+!52>5#"#!5!'7!5L2 &2p"& 2C১  vdd  ,}KK}}KKL/?!2#!"&=46!2#!"&=46!2#!"&=46!2#!"&=462X LLddddddddL/?!2#!"&=46!2#!"&=46!2#!"&=46!2#!"&=46DLDLLddddddddL/?5463!2#!"&5463!2#!"&5463!2#!"&5463!2#!"&Xp LddddddddL/?!2#!"&=46!2#!"&=46!2#!"&=46!2#!"&=462LLLLLddddddddL/?O_o32+"&=46)2#!"&=4632+"&=46)2#!"&=4632+"&=46)2#!"&=4632+"&=46)2#!"&=462ddA ddA ddA ddA LddddddddddddddddL#*:J!#;2+"&=46!2#!"&=465#535!2#!"&=46!2#!"&=46dddd ,XLdddd}KdKddddL#*:J32+"&=46#3!2#!"&=463#'7!2#!"&=46!2#!"&=462ddgdd /ȧ,XLddLdddK}}dddd!2#!"&546 K,,,,,,v,,,D,,L!2#!"&5467'2"&4,XJ*J%pNNpNL d>tNoOOo62.'&54>"264usFE66 !^Xm)!fhHuXyHÂ2".4>"֛[[֛[[Ktrr[֛[[֛oVrru5.54>6?6&'.'&76#&*IOWN>%3Vp}?T|J$?LWPI)(!1 )  HuwsuEG^F&:cYEvsxv!K:%A'# " A)Y l */7>%!2!"3!26=7#!"&546 7l l27);;));Ȼp87cs* s ;) );;)2cL6!#"3!2657#!"&546&'5&>75>^i4);;));ȹpS 9dTX .9I@F* L6;) );;)g  0!;bA4 L5!2!"3!26=7#!"&546 62"/&4?622^^  Ȫ   ȯ  ȭ   ȭ   L326'+"&546d0dLJJL#3266''+"&5462d00dLJJJJ3''&47660J*J36 &546.2   d32+"&546!32+"&546  dL#!"&5463!2L  346&5&5460d * ;O#72#"&5&5&5464646dd12N: 9  > =,L32+"&5&54646Rdd0L;;dH  #!"&762!2#!"&=46  *9HdduJ  u`((&;(J ' 7(a#aa32".4>#"#";;26=326=4&+54&֛[[֛[[}dd[֛[[֛dd2".4>!"3!26=4&֛[[֛[[E [֛[[֛~dd32".4>"'&"2?2?64/764/֛[[֛[[ xx  xx  xx  xx [֛[[֛ xx  xx  xx  xx  $2".4>'&"2764/&"֛[[֛[[Tw[֛[[֛1Uw;K2".4>";7>32";2>54.#";26=4&֛[[֛[[?2".4>#";26=4&#";#"3!26=4&+4&֛[[֛[[    KK  ^  K[֛[[֛V   2  2  2  /_3232++"&=.'#"&=46;>7546+"&=32+546;2>7#"&=46;. g  g g  g Df  fD Df  f g g  g g ͨ  fD Df  fD Df?2".4>"2>4&"/"/&4?'&4?62762֛[[֛[[rrrr@||@||@||@||[֛[[֛;rrrZ@||@||@||@||02".4>"2>4&"/&4?62762֛[[֛[[rrrrjjO[֛[[֛;rrr}jjO!2".4>"&32>54֛[[֛[[KtrAKihstr[֛[[֛;rtxiKA>rtsS6!2#!'&4' &F   &S &5!"&=463!46 &U & U ## ] #!+"&5!"&762   && ]32!2"'&63!46&# U & U # &] &5>746 ^$,[~UU & U #$DuMiqF +!2/"/&4?'&6!"&546762R,^j^!^j^^j^P,^j^IIgg+#!"&546762!2/"/&4?'&6j^^ ,^j^`j^,^^j^/2".4>#";2676&#";26=4&֛[[֛[[:#6#:1  [֛[[֛.   IUaho276?67632;2+"!#!54&+"&=46;2654?67>;26/.'&;26!"&5)#!  &0  =  2 pp 2  =   353  X  v  v !{,  2  ,ԯ  2 0y    r w  +I6.'&&&547>7>'.>7>&67>7>7>-BlabD8=3*U  :1'Ra\{%&=>8\tYR-!q[Fak[)ȕX1 "@&J<7_?3J5%#/D &/q!!6ROg58<'([@1%@_U2]rO.>7'&767>.'&'.'&>77>.'&>' '8GB    `H  >JS>H7 '+" NA 5M[`/Pg!;('2"&"IbYCe\D9$ 886#1%)*J7gG:    8G\au9hoK$]54<&"&5476&2>76&'&6?6&'&'.{nO9:On{{nO:9On{FZ  2Z__Z2  Z# %8-#,- "F-I\b\I*I\b\I--I\b\I*I\b\I9>||;7Es1$F^D10E^E$1u$/D0 "%,I';L!#7.54>327377>76&'&%7.5476&6?'&'.P[vY,9On{R=A &/l'PjR.Mv&  6QFZ  *HLh5)k|# %8- ,- "xatzbI\b\I-yRU4Zrnc1?1FrEs11) ]@ @] )1ES>L'+/37;?CGKOSW[_c3232!546;546;2!546#!"&5353353353353353533533533533535335335335335Rd22ddddddddddd|ddddddddd|ddddddddd2222pddddddddddddddddddddddddddddddw%7&=#!"&=46;3546'#"&=463!&=#'73546oXz#z*dXzdM*zL!2#!#"&546d);;)d);;L;));,;)X);dL ?32!546!32!546".5!2>&54=(LffL(, '6B6'p)IjV\>((>\VjI), +'%! !%'*L 'L'a'M 7 Maa'aQd_)!232"/&6;!%+!!"&5#"&?62**p&032!2#!!2+"&=!"&=#"&/#"&468^&d,!02**6%%+*2222 *L !53463!2!!P;),);DPdd);;)L 3463!2!!;),*:,P, pX);;)dDEk+32"/&6;#"&?62{**YDk&=!/&4?6!546X`)  )   !.#!"!"3!26=4&53353$`$-);;));;ddd-(d;)d);;)d);dddddL #12"&54%##"+"&'=454>;%".=4>7i**d]&/T7 " LRQ  )2( Jf,53232#"./.46;7>7'&6327"&)^Sz?vdjO9t\U>/ v?zS$2451 7F8%M)(  ()GM~ 1==7'''7'7'7'77 N괴N--N괴N-N--N괴N--N괴d!-=32!2+"&/#"&54?>335!7532+"&5462(<H(<,F=-7` 1dd>2vddQ,}Q,d-!2$'$(ddw} L 0<32#!+"&/&546;632+"&546!#35'!5X,<(<(21 `7-=|dd_dd22L!-d,Qv,Q($'$dd dԯ}wdO7G%6!2+#!"&5467!>;26&#!*.'&?'32+"&546dkn  T.TlnTj:d%8   VOddip &yLN(  % H YS(22S dO6F#!"&'#"&463!'&6?6*#!32!7%32+"&546n jUmlT.U  nJ   %&jPddO (SNLy& pd(Y aL7G2#!"&/&?>454&/!7%.!2#!"&=46ސNS( % p &y22SY( nTjkn  T.T8   Vd% dd-I!26=4&#!""&5&/&7>3!2766=467%'^ NLy& p  (S22(SYLddjTnlT.T  nk V   8%d%2".4>%&!"3!7%64֛[[֛[[  [֛[[֛9   &%2".4> 6=!26=4&#!54&֛[[֛[[%  [֛[[֛ &   %2".4>&";;265326֛[[֛[[K &   [֛[[֛@  %2".4>#"#"276&+4&֛[[֛[[  & [֛[[֛  2".4>%&277>7.'.'"'&65.'6.'&767>'&>7>7&72267.'4>&'?6.'.'>72>՛\\՛\\d+: =?1 " "/ ?9 #hu!$ 0 E.(,3)  (     *!A 7 ,8 !?*  \՛\\՛  ' "r"v G  .&* r$>   #1    %  *  '"  $  g2( % 67'"/&47&6PM<;+oX"O\e~Y+" n+We`#'7;!2#!"&=46#3!2#!"&=46!!!2#!"&=46!!d);;));;);;));; );;));;,;)d);;)d);dd;)d);;)d);dd;)d);;)d);dddL !2#!"&46!|;**Dd%32!2!5#!463!54635#!"&=);,); ;),;);));;)d;)pdd);d);dddD);;)+AW!2"/&546)2/"/&4?'&6#!"&54676276#!"&?'&4?622,^j^5,^j^/j^^^^j^j^,^j^&j^,^^^j#;CK2".4>"2>4&$2"&4$2#"'"&546?&542"&4$2"&4ݟ__ݠ^^oooo-- - L- 73H3)z - - - - _ݠ^^ݟWooo -!!- -! $33$ 1~ - - - -Z[%676&'&#"3276'.#"&477>32#"&'&6767632'."[v_"A0?! -  Y7J3$$ )G"#A.,= # (wnkV8@Fv"0DG([kPHNg8B*[eb2!5(7>B3$$' )M"#!7)/c# *xnfL@9NDH7!$W]B$&dXDD>.54>"".#"2>767>54&0{xuX6Cy>>xC8Zvxy#!?2-*!')-?"CoA23:+1! "3)@ +)?jDH-Sv@9yUUy9@vS-H-&65&&56&oM8J41<*.0(@  )*D*2Om9w.2&/7'/&477"/&4?BB8"._{iBBi BBBBBB7._BB^*k"5._{jBBFi BBBBBB77/_2#!"&54>!"264d:;));XV==V=.2G);;)3-D=V==V "/''!'&462*$3, #**#4$*' 2@K#.'#5&'.'3'.54>75>4.&ER<, 3'@" MOW(kVMbO/9X6FpH*M6&+  4C4%dfJ2#4.#"3#>36327#".'>7>'#53&'.>761T^'<;%T)-6"b "S5268 jt&'V7  0 $ݦ -$aPN(?",9J0* d2>2 ""   7Gd/9+DAL!X32"/&6;3+##"&?62*Ȗ*,|%#5##!32"/&6;3353!57#5!ddd,*dc,dd|ddd!%32"/&6;33!57#5!#5##!35*X,ddd,d,ddPdddL32"/&6;3##53#5#!35*Xdddd,d, dPddL32"/&6;3#5#!35##53*d,ddd, ddd32"/&6;3#53!5!!5!!5!*d,dpd , 32"/&6;3!5!!5!!5!#53* dpd,d, LL!2#!"&546!"3!2654&^pg );;));;Lp;) );;));LL+!2#!"&546!"3!2654&&546^pd );;));;oLp;) );;)); $  LL+!2#!"&546!"3!2654&!2"/&6^pg );;));; $ Lp;) );;));LL+!2#!"&546!"3!2654&#!"&?62^pg );;));; p $Lp;) );;));L5!2#!"&=463!2654&#!"&=46&=#"&=46;546&p);;)>DLpd;));d&  #%2"+'&7>?!"'&766763 ,  P'' K    S#  nnV/L5!2#!"3!2#!"&546&=#"&=46;546^>);;)pDLd;) );d&  1!2/"/&47'&6#"3!26=7#!"&5463!m)8m);;));Ȼp,pm)8m;) );;)֥#2".4>"2>4&2"&4ٝ]]ٝ]]qqqq{rrr]ٝ]]ٝGqqqsrrrL#3232"'&6;46!2!54635 ' gdV^|d22L# ++"&=#"&7>!2!54635Gz " 'gdM !d22LK" 62"'&4?62!2!54635qgdq#d22L #'762'&476#"&?'7!2!54635*MMК=gdML*Л:d22L#'/'7'&6"/&4?!2!54635^WЛԛL*MgdКԚPM*MXd22% ! q3gqdL+!#"&546;!3#53LDdddp,E/'&"!#"&546;!3#53"/&4?6262L_  Ȗdddj\jO)_ p,j[jO) >'.!#"&546;!3#53"/"/&4?'&4?62762Lg%dddFF))FF))gp,F))FF))F/!"!#"&546;!3#533232"/&6;546L dddd*p,/'&"!#"&546;!3#53++"&=#"&?62L*ndddd*pp,L !2!546#!"&5!52LPdL&}-1;&=!5!546#"&=46;#5376!!/&4#5;2+p/22ddpddd33*ȖdȖ*yddQ%6+"&5.546%2+"&5.54>323<>3234>^%"% "  d d 1t5gD >?1) A..@  ^  ^ dL3"!5265!3!52>54&/5!"!4"2pK Kp"2KKL8 88 %v% 88 x88 %v% 8LL  $(4!2#5'!7!!2#!"&546!55%!5#!!'!73wipdw%,);;));;),p,ddibbd;) );;));dfdd&767>".'.7.wfw3 .1LOefx;JwF2 1vev/ 5Cc;J|sU@L#A2/.=& &=>2#!"&=46754>ud?,  1;ftpR&mm&L!((" """" '$+  222/2 ! '!'3353353!2+!7#"&46!2!546L J LP*dd*22dL #"!4&#"!4&!46;2d);,;gd);,;;)d);L;));;)D););;)L%)!2#!"&546!#3!535#!#33||D| ,dddL| |||Dddd,ddd,L%)!2#!"&546!#5##3353#33||D| dddddddddL| |||Dddd,L#!2#!"&546!#3!!#3!!||D| ,,L| |||DdddL!2#!"&546!- ||D| ,L| |||D ,L )!2#!"&546!!!#";32654&#||D|dDd&96) )69&L| |||DdVAAT,TAAVL%)!2#!"&546!#3!535#!##53#53||D| ,ddddL| |||Dddd, d dL#'!2#!"&546!3!3##5335#53||D|DdXddd,ddL| |||Dp ddL"&!2#!"&546!#575#5!##53#53||D| d,ddddL| |||Dp2Ȗd d d %2".4>"2>4&!!!'57!۞^^۞^^qqqql,dd,^۞^^۞Lqqqddd '+2".4>"2>4&#'##!35۞^^۞^^qqqql2dddd,^۞^^۞Lqqqd2d2dddddA 62632+54&#!"#"&5467&54>3232"/&6;46n,,.xxPpVAbz  & AwasOEkdb  A32632&"#"&5467&54>++"&5#"&76762n,+.yxZ % OqVAb   AwaxchsOEkdc  dLm%5!33 33!#"!54&#Ԫ2dd,,Md22y7/2#"'2!54635#"&547.546324&546X^Y{;2 iJ7--7Ji/9iJqYZ=gJi22iJX5Jit'*BJb{"&'&7>2"3276767>/&'&"327>7>/&'&&"267"327>76&/&"327>76&/&oOOoSSoOOoS=y" $GF`   Pu "Q9   ccccVQ:   Pu "GF`   y" $ooSWWSo++oSWW"y  `FG # uP  :Q # cccc:Q # uP  $`FG # "y  d "!#5!!463!#53'353!"&5+, ?,dԢdu       d !! 463!#5##5#7!"&=)+5, ?,>dԪ |  ^G |d 77 P#3!#732!!34>3!!ddԢ!,d!s, d,+$d$+ppLL293232#!"&=46;54652#!"'74633!265#535d22s);;);)X>,>XL2dd2;));FD);>XXԢddL6=3232#!"&=46;54652#3#!"&54633!265#535d22s);!);;)X>,>XL2dd2;) $+;) );>XXԢd  #!"&762#";2676&35} ,, }@D:#6#:&77&P'L. dd LL/?O_o32+"&=4632+"&=46!32+"&=4632+"&=46!32+"&=46!32+"&=4632+"&=46!32+"&=46!32+"&=46                  L                  )33#!2!&/&63!5#5353!2+!7#"&46!2!546dd^>1B)(()B1>^dd> J LPdO7S33S7Odd|*dd*22+52#4!!2!'&63!&54!2+!%5#"&46!2!5460P9<:H)"Z" )HJLP;))%&!!&**22$.2"&432!65463!2+!7#"&46!2!546 jjj."+''+# J LPjjj9:LkkL:9r*dd*22,62"&5477'632!65463!2+!7#"&46!2!546X/[3oo"o"."+''+# J LPk6NooN>Qo 9:LkkL:9r*dd*22",!!.54>7!2+!7#"&46!2!546X,%??M<=BmJ J LP9fQ?HSTTvK~*dd*22)2!546754!2#3#3#3#!"&546/R;.6p6.d6\uSpSuu;)N\6226\N)G6.dddddSuuSSudLL/3!2#!"&546!2#!"/!"&4?!"&=46!'|  % XW & dDdL D 2  % XX %  2 dddL#-7!2#4&+"#4&+"#546!2!46+"&=!+"&= Sud;));d;));du);P;ddLuS);;));;)Su ;),); 2222  !&4762 !2!546 'YV/ |UYY(n0U22!/.#!"3!26=326!546;546;33232!'p'q*}20/222,2 "!#!5463!#5!#!"&5463!#5,  w,, v  w, O,T    dGFV32676'&7>++"&?+"'+"&?&/.=46;67'&6;6#";26=4&KjI C   )V=>8'"d 1*) "dT,| -otE  GAkI ! "% ,=?W7|&F@Je5&2WO_e_ 2  2 ~ $4<Rb%6%32!2&'&#!"&=46#";2654&'&"2647>?&/&6%?6'.'.. +jCHf7" *:>XXP* @--@- -?0 !3P/|)( )f!% =  &* x"62&CX>>X83 D-@--@ۂ # =I+E( //}X&+ 5!H d9Q`o322#+"&=#+"&=#"&=46;#"&=46;546;23546!2>574.#!2>574.#q Oh ..40:*"6-@# d   KK   d)  )k)  ) m!mJ.M-(2N-;]<* K  KK  K X K  KK  "p "),!2#!"&'.546"!7.# Vz$RR(z }VG+0 )IU!zV`3BBWwvXZ3Vz&--% ,(1#32#!"&546+"&=ۖgT)>)TH66g )TT)g6633#!"&546+"&=`T)>)TH66B)TT)g66 %'5754&>?' %5%Ndd/\^^<ǔȖ  (Abd 2"&4$2"&4$2"&4|XX|X|XX|X|XX|X X|XX|XX|XX|XX|XX|L2"&42"&42"&4|XX|XX|XX|XX|XX|XLX|XX|X|XX|X|XX|ddLL/!2#!"&=46!2#!"&=46!2#!"&=46}  J    J    J L  p  p  /3!2#!"&546!"3!2654&!2#!"&546!5^ );;)X);; G ;));;)X);d,dddL;!2+32+32+32#!"&46;5#"&46;5#"&46;5#"&46222222222222L********, *.62"&%#462"&%#46"&=32W??WW??||||||*(CBB||||԰||||ӐB76+2+"47&"+".543#"&'&676/!'.6E*  '?) T 0I' *L #3{,# n  6F82 *5#"#!#4.+3#525#"#5!2 &2p"& 2D d 2d  dd R , W 22 L 05"'./#!5"&?!##!"&=463!2E  1;E%= !'y,2 " # 22+."A2VddGJ!2#!"&546#"3!26=4&#"'&?!#"3!26=4&'"'&'#&#2LFF &  7 ? 9   9 gLR   2 2  2 2 $ #'!5!!2#!"&546)2#!"&546!PpmpG,Ld|pd,#'!2#!"&546!2#!"&546!!5!2pmpG,P| pd,dd'+!235463!23##!"&=##!"&546!2dddpdp,d ,'3#3!2#!"&546!!2#!"&546dddpG,|dpd, pdL'+32+!2#!"&5463!5#"&546;53!X|^d,Lpdpdd,'!#3!2#!"&546!!2#!"&546ddvpG,|dpd, p,0o #"&54632a5*A2~ 6'&4O**{))*2A~ !2"'&6d)***2,~o #!"&762{))*a**( 5-5!5!Lc d 1#3!35#5!34>;!5".5323!,P2 &d2"d& 2dd,dd  dd & ,L%1#4.+!52>5#"#!#3!35#5! 2 &d2p"d& 2 ,, dd & ,dd,ddfrJ32 +"'&476 0  ) J 00   >fJ32+"&7 &6S )  0 J ))   fJr"'&=46 4 ))  w  )  0f>J ' &=4762j  00  )  0  =:#463267>"&#""'./.>'&6|Vd&O "(P3G*+*3M, :I G79_7&%*>7F1 ||5KmCKG\JBktl$#?hI7 !2+&5#"&546!5X,p dddL!2%!#4675'=DXDd dQ,[u}4]ddMo__<vsvsQQ(dpEHEd{ d&ndd ddddd5d!u ,d;I]ddQEJadd9'dddd dy'dddddddd,d,A22>ff****NNNNNNNNNNNNNN"~Fn2b\r bb 6 ( L 0  X * ^ h(T*v 8|t*<6`R.j(h6h^2Dl.vb F !2!v!"@""##"#8#z##$$0$^$$%4%`%&&~&'P''(4(p())*&*J*+ +z,,h,,---.(.f..//F/~//0>0011`112$2^223"3>3h344`445,556>6|677N7788B889 9J99::l::;;<:>>?(?n??@H@@AA~BBBCCBCvCCDD`DDEZEFFtFFG6GvGGHH2HNHjHHII8I^IIJJ.JR@. j (|  L 8 x6 6   $ $4 $X | 0 www.glyphicons.comCopyright 2014 by Jan Kovarik. All rights reserved.GLYPHICONS HalflingsRegular1.009;UKWN;GLYPHICONSHalflings-RegularGLYPHICONS Halflings RegularVersion 1.009;PS 001.009;hotconv 1.0.70;makeotf.lib2.5.58329GLYPHICONSHalflings-RegularJan KovarikJan Kovarikwww.glyphicons.comwww.glyphicons.comwww.glyphicons.comWebfont 1.0Wed Oct 29 06:36:07 2014Font Squirrel2       !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~     glyph1glyph2uni00A0uni2000uni2001uni2002uni2003uni2004uni2005uni2006uni2007uni2008uni2009uni200Auni202Funi205FEurouni20BDuni231Buni25FCuni2601uni26FAuni2709uni270FuniE001uniE002uniE003uniE005uniE006uniE007uniE008uniE009uniE010uniE011uniE012uniE013uniE014uniE015uniE016uniE017uniE018uniE019uniE020uniE021uniE022uniE023uniE024uniE025uniE026uniE027uniE028uniE029uniE030uniE031uniE032uniE033uniE034uniE035uniE036uniE037uniE038uniE039uniE040uniE041uniE042uniE043uniE044uniE045uniE046uniE047uniE048uniE049uniE050uniE051uniE052uniE053uniE054uniE055uniE056uniE057uniE058uniE059uniE060uniE062uniE063uniE064uniE065uniE066uniE067uniE068uniE069uniE070uniE071uniE072uniE073uniE074uniE075uniE076uniE077uniE078uniE079uniE080uniE081uniE082uniE083uniE084uniE085uniE086uniE087uniE088uniE089uniE090uniE091uniE092uniE093uniE094uniE095uniE096uniE097uniE101uniE102uniE103uniE104uniE105uniE106uniE107uniE108uniE109uniE110uniE111uniE112uniE113uniE114uniE115uniE116uniE117uniE118uniE119uniE120uniE121uniE122uniE123uniE124uniE125uniE126uniE127uniE128uniE129uniE130uniE131uniE132uniE133uniE134uniE135uniE136uniE137uniE138uniE139uniE140uniE141uniE142uniE143uniE144uniE145uniE146uniE148uniE149uniE150uniE151uniE152uniE153uniE154uniE155uniE156uniE157uniE158uniE159uniE160uniE161uniE162uniE163uniE164uniE165uniE166uniE167uniE168uniE169uniE170uniE171uniE172uniE173uniE174uniE175uniE176uniE177uniE178uniE179uniE180uniE181uniE182uniE183uniE184uniE185uniE186uniE187uniE188uniE189uniE190uniE191uniE192uniE193uniE194uniE195uniE197uniE198uniE199uniE200uniE201uniE202uniE203uniE204uniE205uniE206uniE209uniE210uniE211uniE212uniE213uniE214uniE215uniE216uniE218uniE219uniE221uniE223uniE224uniE225uniE226uniE227uniE230uniE231uniE232uniE233uniE234uniE235uniE236uniE237uniE238uniE239uniE240uniE241uniE242uniE243uniE244uniE245uniE246uniE247uniE248uniE249uniE250uniE251uniE252uniE253uniE254uniE255uniE256uniE257uniE258uniE259uniE260uniF8FFu1F511u1F6AATPwOFF[\FFTMXm*GDEFt DOS/2E`gkcmaprڭcvt (gaspglyfM}]oheadQ46M/hheaQ$ DhmtxROt `locaS`'0omaxpU jnameU,postWH- Ѻ5webf[xTP=vuvsxc`d``b `b`d`d,`HJxc`fft! B3.a0b ?@u" @aF$% 1 x?hSAiSm߽44,qPK q XE](2 .ԩ] "ED i]DԡZJ\8wwV"FpUԯ.Χ(gK4O n;NR{g`'!PMUHEՠJʫ*Yq9c<U9!QIYׅ-KC+ դU)Q94JYp]Nq9.qyVV n)9[{vVכ־FWb++{>׍a|*gQ,K<'W@Ex̢D&Ud# & x Mx2c 5*.lN/h]GtT(xŽ |յ0>wm#Ye[%Y-YR'rYjD% ,@BKZjHڤ@b-R+nhK~룼$;h^fܹsn{ι ˴0 kb8Fd:%Lה"1AՔ AY>,ؔ#pZ4؟5made ?Ȝy=I:C D(nIxL .1!P'JDtHj@L4Ph' )b)vHX,f1c\'cGu>1 ~t?!xT_q?qBF#L%Dћ"?Yǯj??8>NSkemAYDb4 J);@jP$ 'qh8`;aX6CF*dYc"'?hLV㗌,>ce3eVh =C~xC\((qb@ 4xK&hׁ 4\2DZ6N1|-;j Yu@jѫxi䊧mK ٍDEwq3̷.cAw@4t.gkgr{~Wl~{lW2} 276a2\6oz@$HSH gbtX70Ktc1,7B oLƏ66[,%iZ ,l>TpKSGg\> #A#3Eyk6v;u3!ZI8Mk?8CWq{`C*h>H1_skh)ojOO' !~dXgB(0< kOYxeƧĭ5k =d ϧ> +tC-o Ǫ/_koܶs+fOztpu7-}d9 se \9.H4!0S\ ʱk2"?ip7\2zlްt=W\!KyOXimUnov 6: 2 LZkAA^qCޔ &PaFI0>&Q #FQl> A·q*OȦ_@27l,sf 6p7ܩ?M1vA2]$j";vlk~va0gjzRD:gc6yw%g(þ#'uB#=_@?>FVb0a!aL4tXv:Fh9j^xތz}Wn}7}jΚiHitKSaXEEbbBQ1ftxFȮ -"dqA\~F`6i䁕+ Ԣ^Ȳ}ש׆k&Ĺ<- \;g1>w00v^x 7l#Ot^5+xe.^]׼G8^ m(t1 sbfJ %<4H@e8C,5<(kc5YIA]|ךl6+=HVcbKՋB6i4 #_|&>NvQk#pW=u7HɰR$ [5싙 g %19}&@$&l=1RI}9#ςz??1z&ı_ac|PI[:u;l->k4GYm|Zw }HnR=-B ~m.ِ .Mz^,0%8EG**|sg|ozO֬0sz.WN^ yHk<v3t{8-|' ea~H94xA-@y bT4@0b#]DDljDSio:AgSP z:;-|yH"r {B{\5RLi6AAtM]taRKC!1CgC샂 +1EG!Xzٛnzv@x-#i^x*$)W=O\f[WX~V? `Lei::v4$?=Ra#c]8YFJb&'{%LCE Cf]^$/fߪM;À; 6CXV#X~ F< :vCcyBpLv1Fv#9 /8VF01_K?x>}#G7т\Wp!.@bwɡ+{o#ԍPQҮnī66 cZD(. u;nM}?vtxF{+` ="rPπlDV̶?Z@H䰅][35%O )\^ Z;>Ftf-IzӮ yu1uo<:oa:uqwykk ⋜}0?jvX+}VG$s ?26YI5c$Cfb!X*|F^$p7p55߶6[mjgl>* KO& 8ܝ:ǰokKm~oS-*4E}P/% k:e"1AJCAX8= LŢ>ܱav{|K.3 :\Bxwbeb>1ۿvH?f58 %6$ɲ'pL^HXbpIVqnA8Kg'i!UzSEI5N=hpV?(E Vr?޴7Vڋɿ.O;p 4NRZm.O> MuL'j5`;MtAQܶMyV<` $m)yڳXDa:݁q1JFq15-l\3~X-2pFDe/f!2i:=h{%{t^ *PBͽ]YD3jd *w|GLϽ}ˑk7Ç=06oz*zo1~Jw00SePw%#@BJB %+ ';%!& )Hq 7fqH.!Eǎf,9՚$9 H{~i Z)O|!"D.KQ a2 %2Wɂ\{*B{7,9.'ew U^W&$r9rcGBwll<ʷSQゅh! iѨvJ :Y?#_m4q[ },EA{VПP|Dg?9MId?{)/ /\[ Jҏ[f4G>QK^ m O -7w]„<U3jƏ,:Yq~0/mŵ@CCFq{,Θ쬷ΘQSo lsɿh?A2q`5Z&*X1L5:6ς+O]uej%?ۼ&aW?{2[}W?JbΙk-\b7sIkf&Λfx~nO-9V ~cW"ȗy)b\)2MrWf;MU7'[-c/.ؾuMl&.9) G!!W* 60Cф#qrqOKZOWq,8́/XpTȑg<>¤)[J8o` ;S\S%h~p|J˾F~K=E0NQX*8;D7Q1QC% *Eyy} UG?>I`>'6<+3IVgϮyOQ$WBvH v[Ϗ 2+ 'ø6N߆<ɕ 2S娚9X1\┣df>B~-t>W]pPrZ['+ƌl9]8qC!' @AAOuШ !?M\JMͭfǞ)ߕ=w?AN>¼}jQ<ǏpǠ^(}1+2q F4RiHďITr8^!gm>'ڸhE`s̊ol!(9~ o%#)~ƃj$@ՔLpGOa{߿fé)zؔY<~^cs潺ݴNRURTY%8Ks3qd]^QTb' zx)HFҩPmUZjQ&XƁo<0jYGz]$8c&hyݼwΞ{9^sf߹m[vӣ!(ZAsۧyB8RiԣBg6{UmtyW!bpǮd n/ŷʼ@v/%cxEn:4Y²,yZ-krcH&^ȩC'Ȯ'^T5r)((IJU&#݌! +YM.JEX^|Lw@ھZsgY洺\xԟxyLCyo?eV"_[Q/5Y|qI/\9diEBh$v wOL fpa ,?HgHf2RbL v >USo^1/,ēvcYGmŨ~Amz ?/40yj̸pk2H eERb/"M 75ul[drC&Y͐&I `!>p;J-b--.VM4>Fj/5σt5}>C*<'d?,cdGf2ҁ0w6Lh"fKζp;ǿ϶Pdc1EOi%Ř(DCWV2I)TiMFTz0U S7V mBW6;nYZUzSTg>(hF"޽T뽷R]L۶|Lx[s,'NU|E<4)Rp*vU#g*gjə*=~܃ASēA JHw3@NurbwȀʌx}[`7ZtPlh L.)NU}kq'vFQr׷{ˤS]ZL(@*Sf^+uPe_k#.8ɂ%ՠ,@TKх t`ߑXAD;b|pA7}q2 @Y`~iԬK0jY( R~^ҧ8>=F"˜A[DqvQCX|ZsO \/f.F;kPbdz7ԐeͶ-6bybaWjnh7YLF!4wssFCnh_0> MZ nC *#5/OUN\(3o@[7`Mg8xge;f\y|f֤ޑ]i5q5q&>'353kYꭑ=W7+΋yxIeOYǏs(p6[B/t爁*̠-n: <Ц) +ް~q_}oxt>LV FG@d9[2?2ȳ8笞={fgcsCmre#E>45qo:JX^ioP,xf:/yn9VѥS7=u-\%KϦUv,ⳀZ=vkN*+_.ڊ֞iڃ=w @lmr>Oo,VԲɝz &:'45!9pI 0@I[PU""sInvR>A9t$3/|k8yiE c8E!Q\ۂ} %Af4s*A8A΀>D=5uwjnG z?2Q/I=fH4n]澀YmG"2PEHfvZn<šPiA_q/PDտ $$~%NyhrOdM\-m(@\#ƼNJO>a+ uJ*(%¢FPJW,$))} B\_wV] 0TOCÊQ}5{Ho*;;葞rǨMc54S : M7(kY:z`gp Jstˉv'eG^~iD16dA @'N ֭N.?f…1bzJD V o@7R@6<%IF0mj= [}Nۊ57pyv4@<mЭ9Tp?R70қQG[jzib~/)wC? רa-/Cn.ĕH j63pKrhXIƎj o19 f\~:-ѓK47BY̆y%DC~em@]%rs4T G-Ug>HOpVB]{9&^6|m _PLLI7ǒi "'T }? 4|[Fǭtu/_y;Z?HK0Wzc#)~.rĥ+B&JG0[.ΡrOk;VCoX K۝S߳rt:zX\xmJhxNh5 K`;ydp.Ec4XD<-llip.^p: u/.Y[rl_4kz$~Dq]7/T_<菵4K$Ɩ &w S7|K^7MsMGhw㢴0]?fja5aiЦ6C2no• f=)d^v qNcԎl=u]?;f-E~nv}5%Oջd덿=Z%v  nKu ̓*J#1hu1Hr o}SZu=w;nϗU `FȶEn?߫k&l9YdgA8NSGD09MAK{ހK3݊[_]%W4zۈu9\~n3~zir X3k`Psn=m]ԃJksT9deYN`}/]U#b;Rt,lh*#JB+ (iGx\}~IֳFv@Tu֭J @-LwzYgw`wx-(d٢]F3_XcYmQԃWb-F K5d-0b球—֨T+_Zxcj*`}|x~LF*S*oMتAT1p71?R t>R'"Ey)oP7%$rv QeE+nzlVlFrkt''?R'ZCEIKy ga0^}pE;Kq{T/?i"%1ޒb-Ծqƛ˵+ 8]rIڣV{dȪ͜\AQvOS]0.NX9svb?OE~FPU}o[YKrA̓U%7Dw q b/h AhPbQؓJB8I ?I%=XtO;(PhLd S 'hݱ>|TV?,O"\`7.2>D fmg;-C'u, zA`-ټ$x vck2[xp\cbl΀ihsivaÛM,gĨlMz7JvˑVRWϋNo4(-XB^Cl&Vnnn D4[k6N&}f3YQw@$U$(Ǫo:-ZG#&/} ?N}ƥ7A!MhW>?iXprA١b?uϱι-h6;SB#/@ѿJ !%Q)Dq:{JI^ޑˡPY7UG(h?HmъvREH=N`P)QG9FMSMG@2E$Q $s~TkN"9Ն8cF^"?+G٠ ^*gUlFVxUpoC.XCƵ׵͉qK[k[K(l; ӡn%^Rj,$) 1n.G:Cf(,;ĴR—F_~^;իD;6|/jGGSSGGӎļDzbR/X?Up14u$`[ߜH477I~~Irߙs#6+heW6@wK̸h6, 1C"=meA =@z sls];kklr^"s青>&Մ-[{JiҴ9[ݵȩ-]dޢc An۹g}ꒇ6hTɖ?3s^kLcY 1Zn[bݴE߆դwk3f> fMDՠaD ~}&@5u gnOȢ<'` &bӬ-6;X"d*awYvtLXָkUߩa=HR_@+j2T*£%/͸oƤy 19/7 ~7_o+$DүsIH:r yiF:v(dO":omdM8 ;Z9uʩHCg\K/*ԙg*-I_ERqR'[f?GUAovb A$e]/Կo?|ԐQm4G7G833+ 74z*)$݋JpDNj5pqeDf/>%gW{U:g,nlU\t'%E}͝uCꘒܻߺp}U+^b'o(5gVBIOEm>5yzg}AP-P/ޫ6)x5/t;1p1L9Aܳ|)X]mkFEH/4}:,oLMo6]YM50u[yҫfVh?E-A_i﫝j . 6|5`#Z-svfqӟs͚>w7C{ A]Bz,iH'dv?`E x,mz`F[2avhp%(̒ʂ5Ԧ;Gюh\y";|"ٝʖrxzsPHCTvP$ly}iyhvMCr)#x-.(t%fu€(ۅeUUo pqeˡ啗syi Xk`>X@2P. 2͌>n|,/4} ?A&Jr+ɐCV]{Z0- A= F$+%UZyޗٲR B)wT8(aRΣ*-sr5v !^tZ:/K,'F  9=G<Cu"$-FS2(F 0Q+Xw,]=bh[qBQI ;)"Ō926r?}lV =b[j4AzKkQ?T[%$KQ-l_@l/ &;차Dr?P_dE1~z^I~breufP/պ# E+S\G-R4 SSV俑; *`G*5'dL ~ 5Fhb` ꁜ4[b$~GNAX$~ }[W}_z×6m&~O%j/r&|_Sy<-*Lϛ,JQzͤ𫷣|V|GVW~z  HE YnH4r7P?99ߡ|O-5 %4 dzO/4L_PsT>LQD( J8F+)jCb Mu2Xc8$t}&@Qr-֤U_o6q7P1ˤ+rc6I \ (*v24Uc(A ̣93]z;0'=*,e56Va,qh*P@wȬG/Oj|FIm #Pz;Jwʎ}< z Tt~`ȱGP%;? 5((u# vՊI#9,?Gb4K]Qgԟ]E[ phʯG+`Ęp?@>!}" ҽr=CD5 62ZY? iA T(E UJu;"}պ#LcӗVWO&CIԙu8*烞QaQ^*z(L|Jӏ^fp104~CUx*rV*N9π׳Pūsp_L3Z"}&rO|l~kC/Wj><SxMbSg(]J(Z#x\$OC68-f:{Sҳ蚨o4:)Wb"uiuh~d%BAM sWH.gv%4v+=¿ SGϋjWHWu>[B{[uɶs;laziW߭\zC|\fte&ߕ+Bk/t  CM /@S>Tm G`v`?G(,zb" eAAi7QR<"iX:I܋(aV;4R]}^1vԵ7=p|[Jοeµ{)e#ief0KJq"*F#(GjJFhX#шݍk5ERP΋ ^pCeoe:{6۬5͝sƙ8X K6V[=}V+hͧJlZZ5W;TeV-@HID<͙[)֐l^bXeNN"K]@b?.HH gzXaْA}MOeXHNrڟW;htgttOyu3=*פؿCFGsh9JͽZ-k]L-~hii.49Qr5I,Vݓ^jf_},Q6?5NV ޞˍYٜN%ezqƨ>Z Nt1 a %= yhޙ HJZ? hvrk@mY`^insF\*|Lz!/?)(0 MS4(ȗh{-'ho7cCҞ?6'|ubգ@!bÙf{tz1UA?=@ t%䕉iu[ NiD GT@:p<(cXUm2ϱ7zOM^FϴYUfwGs#t:/~Os]Fݑ((^?L$Sʽ WzT>m'_d:5Lh;H7WgzgZZb3{2d5Jj9c+\vqzDbbƶg "l@צpQBbS Q>+d p%}L!cdwHopx(Tpxp#:dvQ qdAQFdLKmPR pU?l zg-jPbGaR&^q>u8p&Ӯф `MGSܵaoWܛZaâٟݰV5Rs2NX qGB OKg BW)Sg\ӡl]z<߲o-_- AKMqӭ!æSigy۰]K;ST'kPqee7cZT{~*7b\H?jٵl3P оwT2jY;)l DueytOTjöUHXgɬ,WϢ^u![]vF| QGh`(# R'5XDQqM6gc'bu:'H( ?yյ6~.e[n *UyZst9R!GMM$xz$]{L<}4JZ~MVՕhy >@u +]2FqO8jѥWCQqrw.䄫ޥ\_y\On)IKGRHŁqI. d+u@ϴ kŤ}9Tv6*xge7?ì}S-AU OMlJ pժݧYwhi6\fAZc,rjFTMj8kO51TqW_n`7%KWsd0:`OXs$4?:SI1W-Pr}² 9.&P^f 8(WI``@5a}ziV pPԽ+:d\j"=aj)W$q{͜p)V|7hj$L֡9\ځn[ k{lG.m m~TEbȭm` wnyP&:PLJY_pNWzVS׃]7Ed%i癬| EWM7r HB6`UGZ 9N2l2ɅHY(ŗiwݓ[`cZR;Yz=TrvH9c. ֲG6*p΅'[:/ҪXCYхMt-']n,{@ cObIN.xN F9뛝NK[Xr=Wm ݏƦY+?sJgXuP%ȗV^[ W;W xvi/XS3ȼ2ԩZ f2/y?8M@Q*˄CXk?MzTy?ZYu׳)]͕1-a7j~ .d  'VztXK2k̹d?zzK.>,BZ`q'kHqy5j>a\C#H;#p7l4} IR7ފ0$=V#_.vs{g>h!Ab/p7=zmi%͟3)^Oj<_UNY63dsIr8EjU* 33|v ;OB@,,\cwd}6k.ukF9'26D]exGJK.׽}S$@ t";2ɩ*41_x7QbjX9Q;#{9eI -奐br B<9dpzIVQ:l+si #=T+R(MDC$ a̱ ONgj19gqXk}FdcG,&..^ɷwwc>E_]3U|t{Jf窂u_.\*W=}lNo+^Ṿ vP>~sTjWz~_ogS}-DTd -TAaYf3,PATcm ռ4g}mE$BwŪ8>9JW⁩O/9PJCXA{,@c,tEJTj98Q& HPl~K%ƞ1ѻ -eD zxNXuz.9}Mc&:Z5ә8% յսmomCB:l8~ܦEjTYHYvnV^IN]]ŽCXkg#s cSB$Ý=$k}cG&/z}_v6<7IVGGg*l\RXST)šE%Yu~Q~>XЅ`9Wk*@_ՊpM]0*%a3X팁KM|{FԔ 췾d7[nlͬD@m8e cż#gHdd@~.jllɛeRcxE(( Km¼GXA7S@[l.%գnMDs]n_Q 5i?zGTG3T@e i,r O2<l+/,%m ۚXn|E]lí[m<|#z+5 7&\5S-{AE^tK M^rq]FmC%2vJ)W-}OM"`9l+=%"T'8zH3QҐѩYP~VزNi 7ۛ ?w1xc`d```d?oAePBYt?;"@.Hc xc`d`` &]aA_x}SJAS<` b)6 >@D"X\o!ι{,_oggg #JVYp>uC4&*<=$g9W@.0q- ;:pt"HUe5 Vg([Ax9!޴EMߗ4N&ӞwjtԞeσLp>w>Gpfz`|^aż>)o oMg+RmRq,RJ1XTN7t{IE\F8U mb:fN&j9Yxc``ЂM /^0Kؘژ0=avcca>bĒIJk ."/ I888qqpnǥ5w)^-8 ||||[5? JPKLpPa) "Z"WDmDWc3K O~/cLuNN+9K8;99/p>"k676-nܷ0h8)iʋK+s9@.xڭNAwh /"TD#J$rqr|!'O3XFާ0wY 1fg;73;3xE0C q=qX4GA$x ZB8ڃ Dw!IaSX w.0?oN؍gڍ@\A`sb k`sݡ},0Ya DȵȵMyFMvYdS20~>/qJG i<#c0C~G9ee Kvв[ڷ{&V(Ө1j1MZqr7,gKܥX0QY{ MYжz=a:[jEݢ BZZ=ns`+ȍxmUSgFB]9I$uw-J;mPwwwwwwwwlޕ]<3)e׿7R^ VV_@$zГ^З~g`0m[czf`(3233 23s2s32  eD*954XXeXX14i++ kk [[ ۲3Qfvd ;1qgg& nLdOboa_c@`PpHhXxNDNdNarsgrgsrsrs rsWrWs rs7r7s rswrwsrOO // oo __ ?? f,˺eݳYϬW;MelP68s䘉GE{RαM 7nܺp;ڛZ[ݛƵ? ѵֵykx~yj?\3V+wE5=QMjzTӣ(vN؉k/셽d/Kd/Kdbbbbbbbbbbjjjjjjjjjj/r{^n/+v ;NaS)ԼffffffffnnnnnnnnnnaaaaaaaChQN-ܩ?C?C?C?C?݇C}>t݇C}C?C?C?C?vNjHMp[qn???????>>=<<<<<:::::::U>::::::::=;;;;;;;;;;;;}VhSoTPwOF2Fl\F M?FFTM `r $e6$t 0 "Q?webfe5옏@? t,3+2q FYO&>bm5ZH$Y{H jd Չ %٧y"+@]e{vNc)n?~?萤h_&iѝ?>^K v-cۍ12Ky,'n(3EwiB& Tlh0M҆dYrﲬnti]yurVXsjgMnәHW r2>iT`V7R(+o6'cB4ι㿚T ]a[Qd<3wq8,rTI80>E?*E痦#7'S ocʷ_7&#*+)+4aA6cy٣f(bF$;{ YA1vP-tG"Cf- WԙuKְK#*K< (Z`٫ [%YT{%Ɋ$s{oջvt"p4`ߩϤ}o `'ne> G5sz_N PKӦvmU ɾ{z"3`l W#Ԑ^@+,ckoAOpnuzzJ)Υ1}O=xR`J`qUs/+kv1xljlEl\nDƶVjg{Zdz7 5!xm5o[u&1ڂHBkAqrR (\gh7Ҋy=HZUPh$8RgzgͭN:1u$܅>R]"f7 K^'3+E/^YU5]NB.ʋ8+͏8,|{M|Aua|a˅՝% lKGP,Nukc8mX@d̘?Y&{?P(G]Or-\LF9,&y8r3ܟ?p>~sDz1?\U5q=tzԒ&Znj%mM"}tkDwh-=mB76&:һqt" 1:Еu;"K_/Jdc0l0'^B8VCzg[ ;d Ybȃuu;@*}y| .'C>\g=9VŐ[o|g^ >d 9 *E|A*M[[*mOQz?Pn?R)YoT&[U*5S MB [ oYDh{,}1f?NN ]O/^;\J BEsJrĚ'g/B%o Cn7:|yKt&$s|wP\i]$Z@+ Հ90x]r%+RUEm+ܰ;wu9/I77զQlu\yWN)8ܰvY*umm( fEG8 j#IRz #q߷ )Y$ Лc_%m-{!0-` ;公hyV]Hv! ta\K[1{"j 6@3T0%Θ"ԙZIGS.ΣpӬS1eٓ؛ Yv8d\BlSR)ӆ {Iӆ%>0Ўڦ\'cg2%4QD 0͒3B"MՎ&ۊhIڧRgME I(5UD] }b8$8>X h"l΀j.%ۀHH- Iݸ#1C4Y7YݖV o>P]6O47f ~AJdYF€.oy) 8l 22e1H[t@!ȅ 2\@5ٓ%Zkޒa@.`n3OFR(󅥶ZkLkF HWjY I5*6eSbk.5F,.N0ԙ|V||~N( 4],Jp|~xeA5/ڻSvy?'_v|rXHQēB@= XB94TBBcHP+_YH#$`FB;+BPR4̼ t:t"ZEJ^!XǓq4_dTW(5܀IUŇAz@U6n.WGXHRK&'swMjʎ<3)`#F@  F Ԣvob$x +u&}|X&[٪8F-E&/>/G.az^/})'x$O=<zoA9M؝&~3r3g'8ң\-MDzk5A G9|1-! 87[,mRu|57 =X,aJ^tN4\fЄ]AzH^7F&k"LU>}>rBX(ۂT% JdhKPKTFaA3HHC[r;ad54 lLkjG{8h~ fR@9wB0 zS'a7@@Nƹlbj3hNXF/es'DsQjw}Jz^:V.:ڋ{ͼ(ȲBɦx<Db#"S{PHuN/{r6;wUsPО p8+6g_2lΡ6H džH: dBtGNmx@j |{s9=wR/oDJs5z>;'xEq^r^=G?9AA_K%Dɮ:uikjkIeG՝#*)jm|t}`JZ؈H=4{g߁)qXMA,H71V"o,Y#hݨS_;a_ԗZ^cn4HE?} ȝ٤=}BWvުUehGF;@2S@f n2#fY:]JyH]-G׌wgv'|0e _7Ґn+fٸY<( ?y%wm+j&&!c^u'b&hm6¤*2 ?AIƲ5FWؙ[ƜBUzIE!m:xheǮnz|]% mrUFگ1 };!n F&gP;&$$F).tBQ3(C=Xes;iي@~NΡE SRh\BeobTnΒju g@'qQ딎nx.u6bVU& ];!C_  5*zɺmRQuqPZ0}mn^nOrT:U'h0nZp^R|DF_b\@mDE8{oGM᠜q}Sd C,iܚE/Ë[d8],MCI_u,]Vc"pg@`"y),;B^el2'.(Ęy>-|hw;jՍiԽ_o|!@)ɢ=̌SPz*!z})|ƧT}jEtCZný*՞4ۆ׽[ 9Юݓz`Wmeo|j8j59@.EV/ZW@|f_\"${v/;a:Sei3TG*]ơ/h2C32$1}DNXt?Fϝ~n,Pj9.>ף{ 9EN-v|3hCиE XT;P$=J-gݕigz~q(A<:h193N̽Q}CLWߧ׎~ b"|4u}cy62[ \d,ҎճbkD%0Tx{=;Է(i LS13Nh/6?'E^~P{sZZKĞB{Dt&z)Uoa5Q3ȗr~ F]$<tm(} MB@[GxFh8#},#u Laz(Qh4%xm`Uչ.Ev1a4_'/[d{FxI59 D<&8VEFg 芘#I䟍2S_]QqAn_Q>bޘ4g-0&E#ci8 vR/4rP7KsOWN3ՏvE\bqQ5ZڽVy5]h/ i)-/kNю#e)"P {KSQx>a&, _g-mc<n]Ч-52cz 7d PzVOPvfR Rఓ9Z -dC`,at=k?v4#P Bإ/[s.-bH)ɺz '}׶w!rXZ .:Vn;->: 6rUcs4kVW{#5ߑ0B`ܝ0u".QdB0Cr]#Q9lqN^ֳh~NU\ 16 ~SnTl\THҲڛ-~G~)$oQ7-C}q%/avO|[q4~Bc-$N76w{V餃.&(o*n NeRi4!3R"4nbm-y[X."!QKE\N4gՠםaNp >k)90BZBs yrer)vDtrv\v[>rJm a̼~uՏ>rMZcB<`)\yt|ۍr'<>[Îh7Z8caI! p⢟̮,G k5@`iw nО8pv *'O A[.rhT pR?+;\*HsLqUf:ql-ć *6!h+ˬ{h- jgkMMP#:}{/VŶC]옙&[W$ګ^#4fWa\ 5躺M[6)T3~ :. Z`si(RQ|/` il^L#f-;-C;_*{@EMCooÂ_7TrqzF%ׯ|UEƫUs^ݜv{fQ<ĐVPTfͦ?mpP*&QG{cJEPe2)xP0AMɪZHj"׻"AC+zqmVzᖞU%C:@1W [y)J@ob% jA>)Nǀi$At`>?f0gH36p6D|M 4N 4JJڃ jƇ\ p38Я6pV?:$sDNƹ2n,HO\[ոK-)W~im?T:޺UeY-#dJe)Z5?$\dW<,Ɇ;ط5SոTT̄f(PYv=Q ~DX*8辩s- ˨΀55 XRl QC l|5{ӦT\t꼕+en۸Psl3UO[ZS3*,:ÛZLS'̵**@ı~xgno2- WV;pZ9?~$6҄xJ>\QA_Cihbl] 64*A˯ɰqX7YX.-ոaɇVhiKgqNRĆN(r']%٘@3̀jZJ.;nm,S0xͻOF33ҧ<$'GE+}'1f3y5/&Z\RB7dm]8\3߂Ȫ@oT3eu^W@e7l!B,s1$Z&?dC (YЦSm>J"&pt܈P㇄BF4G5 t^Ć$j-a㠍g^ʐCAsT=kTS,|r9IBϘЬ'vGA@thQNj&T=xt;2]P|T- LÞe1ݽWZŚ*MrH5?=o"9K5='k-*AE| qҔ_?\7%|M6f++S*}W_]3fmܮ˳m w!.R#鬪;qq71$•ݙկ_iK&JάMemV5P0> Q5WHIh&4ҍIlE7}sm[cȾ|d^ %Uv1D>.T7*=tZ_㟾1Х:=0pZ6ҋNt(uƝ; B]$kڌ.{F*/UZN砦|oqKG;^侞9NexK \wh~ZpHb䉸 [k8k.bX.QXpxYa^"#Bwnbum5F~>8bN:p4 [gv^ BFUz)?60F8/2C8>N8G%l%5FH{46h4%# 7x oN t\'Ȩ E0#jNãVӹd?WlcW žֵu-}22EN}#䵵2H^a3rqs-S3&f퇣fwl.=W8,cHjcTWנs90ZDMC2ZMdjt"8:g{.Ʊ1Fb618"yԦ> W9 V `jT򔔑r,ni d qN .g+ S Q KaB?_QE rjh>Eӛ;C׭7^q `Ue#-;oJċԝ>) ;Jg׭9R;OgiI7}8Kہqjeؓ+ٗ'nϷk3eFρ0V#pMAzb^PVu~1uғwn ^.II_vdW[Q,+Lbćq 9V} ΏVw4qU3&jıHYb ttT7ρarBwP9?)uT/aA19kM \Psq+=[5͔?9W+^o^E8s)f 2aQxi& NE>"^Naa;f9]NE& t^CLz'e8ZRs&67_ãcyJ1 @TZ?SD2 |POӌ\dR7zH9iQ#zrc.4GR4qx<2~Xhnੳ2auBNC+kX0 aj5n>މe3vާ<>_ uH:XR%~9!4oѼ38? 1d#A&{A!i6 /Xa㇤=W;|) g~ ?*悽 }ڧKt>5|E.A Q6 (6 6є7<9_C f1Ўi8, V4$uti,.`v6r P gFBɎ t C3; ,oÂx| /KMp1S_X.fV#U>Ȓ#B] AIVoІϵGTV1nr+OXS% ³fOZ[_9P߰ {Gln%#hdwH= ye/W>,IP,*MV~ºK&eċM콣=)qFS"GTF*LX,h[wweWQEx ?{^چExhiׂJH|^͓e*^Я.uxEb#;ԝ<]z]\wNhochqE=4Q17W̓lÕ6᧿HE_̣qy YR۫9~l4sVy`Uߛ,#_u+DeM~hq벇#Yz$; 5ͯ9$ z> *jO$$O/xRtf-}*oɦ|3M;xިUl/.~XǎY4x3&x";$KI5dڭ ~w[M9O%4Q}S^t@w[Y;-s;bwH-* imI-1e/~TNN.p)H$W~ƦO (9, ]gM6r+#%/swA$q4O> d9}+$s?0a,>yڈs<=,c_*\D}2MT8/4g'ڦ8'}"C*\9#Y>z$7c[s|"$} ymzQx 5%o$jkp)x-:И|?ofgFr2SZq}q o,wyOgCF1l'L5T33yM92"s5uD6-JUbs O)wR -2/5frϛf@=BFCB&'F}@&yubC?'S49+ÓCIî+f/RU C Fu:C*} T:}{ݽⲷue[!>? ڸ"M 8gz0\HkZ:h~@+#N fjyio!B R'5>`[!T`mC Iѝ}n >W!M}Uav43)!kcȂm? dwv!ה;Xϡۨ}8vt"Ӽ# kvXJ[l[ZݙMÀXC3l[ TaVjʻѬ"œ t:(<cZveQTqHi{銀Q埓'ÖiP■mKAIBF =Tᅽ(&TS?/؁A:ַОV(@wFa^]o]*99Ri_2vM`Pf{QYH#V7v7Ұq>@~uɘ׆Ax/xB3Ġtyb0nG` EDٍA: PwI7nW2ED}.(h"U]9Ih_V@GZ0C pb :L 3tN*N 2!3 Cayn.ɋW`̳}QBCi 8*{57O#aTBUoi0 _^ ChrU}~rL 1z>..=%GG o EuPPsؘ޸8Pu&;*|i&Pbțh;[|y*cVhҼ(~_AqU2GIQ3`^v=@K'ЇZ#4sJ=:sY sڥbyj S_E܃"@~>86#y[cSŬ#SJGZyvvSя扝pwaT/, 9'Jkv%%.~o[ 衧RBjSȀ*$'腁pçSu +9\_f+8u\,tpэkخJ0h(]NQvW7 86:ݣ WcY_i>"R(e]6RA%U6&F]7@̳k3X h?KQ2Bk[?..KKAb65ke+]FeWHU0Oק5 e3Hco>l]02cH9{Z {sO!A,7?ŷ3w俎A Fj8B&8U$G$Y5FL5n1> q2.6e +@/kb{(7i={l͍݂濦81g(%h/EfMҍt5̼vgo ~ਜ਼WKi父UأݖwRSEFT% `=|*=1*SX^w)lfQH(YSSˌK1W]f7ך^&p@T'.%3 5zaTf6A5LX̡|L-ηTg{A)F."hjA;.~o% G#}&]׾c`ChH9xnNY lc\+v\EƧ1D9KX)2b.NWQש$/|6tð32ԛ72иyu0e)Nuh'd~xY ># b"k3 :9v$ПC:)H> զz;ed\jmfOa%9cKxۥ!k%HDn{Y"{n_} )9= _/Z(>lYVgQ#߭:Qbw$zwٮ#U?|Ghz{o$wϜ)|Vh? ZV7%Go/׆E"KӲlp76-z !l4n>$\zV?szqejQ]m^=^ !lHB4sL i9}2^K5OB)O v^~݀xrm\K&G^5CL}&FB]Kn3|sGjykObsܽaW?R6Jfh2 lBS\=jV*Y^˺^E)*\ rr(a@6nԌ?}dLgIvqNcaƮkmLcA!hdVwc=憖s_:җsLg>1*4-%&0Ub)Eܬ*b51 ++;<`!qfM*,[/GK+{,>CLR%%c~'EGAG=h䟔8:IDN)W̻AF)ucw'qhXèL@a~6Pc2L"A2bU & 9A#QLO:E9kfKFb93tL$cˬpLz5dp۰>$`.~X=?NͰ/LPNo0p b8AR4r Jj} Ӳ04ˋquۏAFP 'HfXDIVTM7Lv\(N,/ʪnڮi^m?~ QU Ӳ04ˋquۏb$tV&gϖr>3)throw new Error("Bootstrap's JavaScript requires jQuery version 1.9.1 or higher, but lower than version 4")}(jQuery),+function(a){"use strict";function b(){var a=document.createElement("bootstrap"),b={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd otransitionend",transition:"transitionend"};for(var c in b)if(void 0!==a.style[c])return{end:b[c]};return!1}a.fn.emulateTransitionEnd=function(b){var c=!1,d=this;a(this).one("bsTransitionEnd",function(){c=!0});var e=function(){c||a(d).trigger(a.support.transition.end)};return setTimeout(e,b),this},a(function(){a.support.transition=b(),a.support.transition&&(a.event.special.bsTransitionEnd={bindType:a.support.transition.end,delegateType:a.support.transition.end,handle:function(b){if(a(b.target).is(this))return b.handleObj.handler.apply(this,arguments)}})})}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var c=a(this),e=c.data("bs.alert");e||c.data("bs.alert",e=new d(this)),"string"==typeof b&&e[b].call(c)})}var c='[data-dismiss="alert"]',d=function(b){a(b).on("click",c,this.close)};d.VERSION="3.3.7",d.TRANSITION_DURATION=150,d.prototype.close=function(b){function c(){g.detach().trigger("closed.bs.alert").remove()}var e=a(this),f=e.attr("data-target");f||(f=e.attr("href"),f=f&&f.replace(/.*(?=#[^\s]*$)/,""));var g=a("#"===f?[]:f);b&&b.preventDefault(),g.length||(g=e.closest(".alert")),g.trigger(b=a.Event("close.bs.alert")),b.isDefaultPrevented()||(g.removeClass("in"),a.support.transition&&g.hasClass("fade")?g.one("bsTransitionEnd",c).emulateTransitionEnd(d.TRANSITION_DURATION):c())};var e=a.fn.alert;a.fn.alert=b,a.fn.alert.Constructor=d,a.fn.alert.noConflict=function(){return a.fn.alert=e,this},a(document).on("click.bs.alert.data-api",c,d.prototype.close)}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.button"),f="object"==typeof b&&b;e||d.data("bs.button",e=new c(this,f)),"toggle"==b?e.toggle():b&&e.setState(b)})}var c=function(b,d){this.$element=a(b),this.options=a.extend({},c.DEFAULTS,d),this.isLoading=!1};c.VERSION="3.3.7",c.DEFAULTS={loadingText:"loading..."},c.prototype.setState=function(b){var c="disabled",d=this.$element,e=d.is("input")?"val":"html",f=d.data();b+="Text",null==f.resetText&&d.data("resetText",d[e]()),setTimeout(a.proxy(function(){d[e](null==f[b]?this.options[b]:f[b]),"loadingText"==b?(this.isLoading=!0,d.addClass(c).attr(c,c).prop(c,!0)):this.isLoading&&(this.isLoading=!1,d.removeClass(c).removeAttr(c).prop(c,!1))},this),0)},c.prototype.toggle=function(){var a=!0,b=this.$element.closest('[data-toggle="buttons"]');if(b.length){var c=this.$element.find("input");"radio"==c.prop("type")?(c.prop("checked")&&(a=!1),b.find(".active").removeClass("active"),this.$element.addClass("active")):"checkbox"==c.prop("type")&&(c.prop("checked")!==this.$element.hasClass("active")&&(a=!1),this.$element.toggleClass("active")),c.prop("checked",this.$element.hasClass("active")),a&&c.trigger("change")}else this.$element.attr("aria-pressed",!this.$element.hasClass("active")),this.$element.toggleClass("active")};var d=a.fn.button;a.fn.button=b,a.fn.button.Constructor=c,a.fn.button.noConflict=function(){return a.fn.button=d,this},a(document).on("click.bs.button.data-api",'[data-toggle^="button"]',function(c){var d=a(c.target).closest(".btn");b.call(d,"toggle"),a(c.target).is('input[type="radio"], input[type="checkbox"]')||(c.preventDefault(),d.is("input,button")?d.trigger("focus"):d.find("input:visible,button:visible").first().trigger("focus"))}).on("focus.bs.button.data-api blur.bs.button.data-api",'[data-toggle^="button"]',function(b){a(b.target).closest(".btn").toggleClass("focus",/^focus(in)?$/.test(b.type))})}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.carousel"),f=a.extend({},c.DEFAULTS,d.data(),"object"==typeof b&&b),g="string"==typeof b?b:f.slide;e||d.data("bs.carousel",e=new c(this,f)),"number"==typeof b?e.to(b):g?e[g]():f.interval&&e.pause().cycle()})}var c=function(b,c){this.$element=a(b),this.$indicators=this.$element.find(".carousel-indicators"),this.options=c,this.paused=null,this.sliding=null,this.interval=null,this.$active=null,this.$items=null,this.options.keyboard&&this.$element.on("keydown.bs.carousel",a.proxy(this.keydown,this)),"hover"==this.options.pause&&!("ontouchstart"in document.documentElement)&&this.$element.on("mouseenter.bs.carousel",a.proxy(this.pause,this)).on("mouseleave.bs.carousel",a.proxy(this.cycle,this))};c.VERSION="3.3.7",c.TRANSITION_DURATION=600,c.DEFAULTS={interval:5e3,pause:"hover",wrap:!0,keyboard:!0},c.prototype.keydown=function(a){if(!/input|textarea/i.test(a.target.tagName)){switch(a.which){case 37:this.prev();break;case 39:this.next();break;default:return}a.preventDefault()}},c.prototype.cycle=function(b){return b||(this.paused=!1),this.interval&&clearInterval(this.interval),this.options.interval&&!this.paused&&(this.interval=setInterval(a.proxy(this.next,this),this.options.interval)),this},c.prototype.getItemIndex=function(a){return this.$items=a.parent().children(".item"),this.$items.index(a||this.$active)},c.prototype.getItemForDirection=function(a,b){var c=this.getItemIndex(b),d="prev"==a&&0===c||"next"==a&&c==this.$items.length-1;if(d&&!this.options.wrap)return b;var e="prev"==a?-1:1,f=(c+e)%this.$items.length;return this.$items.eq(f)},c.prototype.to=function(a){var b=this,c=this.getItemIndex(this.$active=this.$element.find(".item.active"));if(!(a>this.$items.length-1||a<0))return this.sliding?this.$element.one("slid.bs.carousel",function(){b.to(a)}):c==a?this.pause().cycle():this.slide(a>c?"next":"prev",this.$items.eq(a))},c.prototype.pause=function(b){return b||(this.paused=!0),this.$element.find(".next, .prev").length&&a.support.transition&&(this.$element.trigger(a.support.transition.end),this.cycle(!0)),this.interval=clearInterval(this.interval),this},c.prototype.next=function(){if(!this.sliding)return this.slide("next")},c.prototype.prev=function(){if(!this.sliding)return this.slide("prev")},c.prototype.slide=function(b,d){var e=this.$element.find(".item.active"),f=d||this.getItemForDirection(b,e),g=this.interval,h="next"==b?"left":"right",i=this;if(f.hasClass("active"))return this.sliding=!1;var j=f[0],k=a.Event("slide.bs.carousel",{relatedTarget:j,direction:h});if(this.$element.trigger(k),!k.isDefaultPrevented()){if(this.sliding=!0,g&&this.pause(),this.$indicators.length){this.$indicators.find(".active").removeClass("active");var l=a(this.$indicators.children()[this.getItemIndex(f)]);l&&l.addClass("active")}var m=a.Event("slid.bs.carousel",{relatedTarget:j,direction:h});return a.support.transition&&this.$element.hasClass("slide")?(f.addClass(b),f[0].offsetWidth,e.addClass(h),f.addClass(h),e.one("bsTransitionEnd",function(){f.removeClass([b,h].join(" ")).addClass("active"),e.removeClass(["active",h].join(" ")),i.sliding=!1,setTimeout(function(){i.$element.trigger(m)},0)}).emulateTransitionEnd(c.TRANSITION_DURATION)):(e.removeClass("active"),f.addClass("active"),this.sliding=!1,this.$element.trigger(m)),g&&this.cycle(),this}};var d=a.fn.carousel;a.fn.carousel=b,a.fn.carousel.Constructor=c,a.fn.carousel.noConflict=function(){return a.fn.carousel=d,this};var e=function(c){var d,e=a(this),f=a(e.attr("data-target")||(d=e.attr("href"))&&d.replace(/.*(?=#[^\s]+$)/,""));if(f.hasClass("carousel")){var g=a.extend({},f.data(),e.data()),h=e.attr("data-slide-to");h&&(g.interval=!1),b.call(f,g),h&&f.data("bs.carousel").to(h),c.preventDefault()}};a(document).on("click.bs.carousel.data-api","[data-slide]",e).on("click.bs.carousel.data-api","[data-slide-to]",e),a(window).on("load",function(){a('[data-ride="carousel"]').each(function(){var c=a(this);b.call(c,c.data())})})}(jQuery),+function(a){"use strict";function b(b){var c,d=b.attr("data-target")||(c=b.attr("href"))&&c.replace(/.*(?=#[^\s]+$)/,"");return a(d)}function c(b){return this.each(function(){var c=a(this),e=c.data("bs.collapse"),f=a.extend({},d.DEFAULTS,c.data(),"object"==typeof b&&b);!e&&f.toggle&&/show|hide/.test(b)&&(f.toggle=!1),e||c.data("bs.collapse",e=new d(this,f)),"string"==typeof b&&e[b]()})}var d=function(b,c){this.$element=a(b),this.options=a.extend({},d.DEFAULTS,c),this.$trigger=a('[data-toggle="collapse"][href="#'+b.id+'"],[data-toggle="collapse"][data-target="#'+b.id+'"]'),this.transitioning=null,this.options.parent?this.$parent=this.getParent():this.addAriaAndCollapsedClass(this.$element,this.$trigger),this.options.toggle&&this.toggle()};d.VERSION="3.3.7",d.TRANSITION_DURATION=350,d.DEFAULTS={toggle:!0},d.prototype.dimension=function(){var a=this.$element.hasClass("width");return a?"width":"height"},d.prototype.show=function(){if(!this.transitioning&&!this.$element.hasClass("in")){var b,e=this.$parent&&this.$parent.children(".panel").children(".in, .collapsing");if(!(e&&e.length&&(b=e.data("bs.collapse"),b&&b.transitioning))){var f=a.Event("show.bs.collapse");if(this.$element.trigger(f),!f.isDefaultPrevented()){e&&e.length&&(c.call(e,"hide"),b||e.data("bs.collapse",null));var g=this.dimension();this.$element.removeClass("collapse").addClass("collapsing")[g](0).attr("aria-expanded",!0),this.$trigger.removeClass("collapsed").attr("aria-expanded",!0),this.transitioning=1;var h=function(){this.$element.removeClass("collapsing").addClass("collapse in")[g](""),this.transitioning=0,this.$element.trigger("shown.bs.collapse")};if(!a.support.transition)return h.call(this);var i=a.camelCase(["scroll",g].join("-"));this.$element.one("bsTransitionEnd",a.proxy(h,this)).emulateTransitionEnd(d.TRANSITION_DURATION)[g](this.$element[0][i])}}}},d.prototype.hide=function(){if(!this.transitioning&&this.$element.hasClass("in")){var b=a.Event("hide.bs.collapse");if(this.$element.trigger(b),!b.isDefaultPrevented()){var c=this.dimension();this.$element[c](this.$element[c]())[0].offsetHeight,this.$element.addClass("collapsing").removeClass("collapse in").attr("aria-expanded",!1),this.$trigger.addClass("collapsed").attr("aria-expanded",!1),this.transitioning=1;var e=function(){this.transitioning=0,this.$element.removeClass("collapsing").addClass("collapse").trigger("hidden.bs.collapse")};return a.support.transition?void this.$element[c](0).one("bsTransitionEnd",a.proxy(e,this)).emulateTransitionEnd(d.TRANSITION_DURATION):e.call(this)}}},d.prototype.toggle=function(){this[this.$element.hasClass("in")?"hide":"show"]()},d.prototype.getParent=function(){return a(this.options.parent).find('[data-toggle="collapse"][data-parent="'+this.options.parent+'"]').each(a.proxy(function(c,d){var e=a(d);this.addAriaAndCollapsedClass(b(e),e)},this)).end()},d.prototype.addAriaAndCollapsedClass=function(a,b){var c=a.hasClass("in");a.attr("aria-expanded",c),b.toggleClass("collapsed",!c).attr("aria-expanded",c)};var e=a.fn.collapse;a.fn.collapse=c,a.fn.collapse.Constructor=d,a.fn.collapse.noConflict=function(){return a.fn.collapse=e,this},a(document).on("click.bs.collapse.data-api",'[data-toggle="collapse"]',function(d){var e=a(this);e.attr("data-target")||d.preventDefault();var f=b(e),g=f.data("bs.collapse"),h=g?"toggle":e.data();c.call(f,h)})}(jQuery),+function(a){"use strict";function b(b){var c=b.attr("data-target");c||(c=b.attr("href"),c=c&&/#[A-Za-z]/.test(c)&&c.replace(/.*(?=#[^\s]*$)/,""));var d=c&&a(c);return d&&d.length?d:b.parent()}function c(c){c&&3===c.which||(a(e).remove(),a(f).each(function(){var d=a(this),e=b(d),f={relatedTarget:this};e.hasClass("open")&&(c&&"click"==c.type&&/input|textarea/i.test(c.target.tagName)&&a.contains(e[0],c.target)||(e.trigger(c=a.Event("hide.bs.dropdown",f)),c.isDefaultPrevented()||(d.attr("aria-expanded","false"),e.removeClass("open").trigger(a.Event("hidden.bs.dropdown",f)))))}))}function d(b){return this.each(function(){var c=a(this),d=c.data("bs.dropdown");d||c.data("bs.dropdown",d=new g(this)),"string"==typeof b&&d[b].call(c)})}var e=".dropdown-backdrop",f='[data-toggle="dropdown"]',g=function(b){a(b).on("click.bs.dropdown",this.toggle)};g.VERSION="3.3.7",g.prototype.toggle=function(d){var e=a(this);if(!e.is(".disabled, :disabled")){var f=b(e),g=f.hasClass("open");if(c(),!g){"ontouchstart"in document.documentElement&&!f.closest(".navbar-nav").length&&a(document.createElement("div")).addClass("dropdown-backdrop").insertAfter(a(this)).on("click",c);var h={relatedTarget:this};if(f.trigger(d=a.Event("show.bs.dropdown",h)),d.isDefaultPrevented())return;e.trigger("focus").attr("aria-expanded","true"),f.toggleClass("open").trigger(a.Event("shown.bs.dropdown",h))}return!1}},g.prototype.keydown=function(c){if(/(38|40|27|32)/.test(c.which)&&!/input|textarea/i.test(c.target.tagName)){var d=a(this);if(c.preventDefault(),c.stopPropagation(),!d.is(".disabled, :disabled")){var e=b(d),g=e.hasClass("open");if(!g&&27!=c.which||g&&27==c.which)return 27==c.which&&e.find(f).trigger("focus"),d.trigger("click");var h=" li:not(.disabled):visible a",i=e.find(".dropdown-menu"+h);if(i.length){var j=i.index(c.target);38==c.which&&j>0&&j--,40==c.which&&jdocument.documentElement.clientHeight;this.$element.css({paddingLeft:!this.bodyIsOverflowing&&a?this.scrollbarWidth:"",paddingRight:this.bodyIsOverflowing&&!a?this.scrollbarWidth:""})},c.prototype.resetAdjustments=function(){this.$element.css({paddingLeft:"",paddingRight:""})},c.prototype.checkScrollbar=function(){var a=window.innerWidth;if(!a){var b=document.documentElement.getBoundingClientRect();a=b.right-Math.abs(b.left)}this.bodyIsOverflowing=document.body.clientWidth
    ',trigger:"hover focus",title:"",delay:0,html:!1,container:!1,viewport:{selector:"body",padding:0}},c.prototype.init=function(b,c,d){if(this.enabled=!0,this.type=b,this.$element=a(c),this.options=this.getOptions(d),this.$viewport=this.options.viewport&&a(a.isFunction(this.options.viewport)?this.options.viewport.call(this,this.$element):this.options.viewport.selector||this.options.viewport),this.inState={click:!1,hover:!1,focus:!1},this.$element[0]instanceof document.constructor&&!this.options.selector)throw new Error("`selector` option must be specified when initializing "+this.type+" on the window.document object!");for(var e=this.options.trigger.split(" "),f=e.length;f--;){var g=e[f];if("click"==g)this.$element.on("click."+this.type,this.options.selector,a.proxy(this.toggle,this));else if("manual"!=g){var h="hover"==g?"mouseenter":"focusin",i="hover"==g?"mouseleave":"focusout";this.$element.on(h+"."+this.type,this.options.selector,a.proxy(this.enter,this)),this.$element.on(i+"."+this.type,this.options.selector,a.proxy(this.leave,this))}}this.options.selector?this._options=a.extend({},this.options,{trigger:"manual",selector:""}):this.fixTitle()},c.prototype.getDefaults=function(){return c.DEFAULTS},c.prototype.getOptions=function(b){return b=a.extend({},this.getDefaults(),this.$element.data(),b),b.delay&&"number"==typeof b.delay&&(b.delay={show:b.delay,hide:b.delay}),b},c.prototype.getDelegateOptions=function(){var b={},c=this.getDefaults();return this._options&&a.each(this._options,function(a,d){c[a]!=d&&(b[a]=d)}),b},c.prototype.enter=function(b){var c=b instanceof this.constructor?b:a(b.currentTarget).data("bs."+this.type);return c||(c=new this.constructor(b.currentTarget,this.getDelegateOptions()),a(b.currentTarget).data("bs."+this.type,c)),b instanceof a.Event&&(c.inState["focusin"==b.type?"focus":"hover"]=!0),c.tip().hasClass("in")||"in"==c.hoverState?void(c.hoverState="in"):(clearTimeout(c.timeout),c.hoverState="in",c.options.delay&&c.options.delay.show?void(c.timeout=setTimeout(function(){"in"==c.hoverState&&c.show()},c.options.delay.show)):c.show())},c.prototype.isInStateTrue=function(){for(var a in this.inState)if(this.inState[a])return!0;return!1},c.prototype.leave=function(b){var c=b instanceof this.constructor?b:a(b.currentTarget).data("bs."+this.type);if(c||(c=new this.constructor(b.currentTarget,this.getDelegateOptions()),a(b.currentTarget).data("bs."+this.type,c)),b instanceof a.Event&&(c.inState["focusout"==b.type?"focus":"hover"]=!1),!c.isInStateTrue())return clearTimeout(c.timeout),c.hoverState="out",c.options.delay&&c.options.delay.hide?void(c.timeout=setTimeout(function(){"out"==c.hoverState&&c.hide()},c.options.delay.hide)):c.hide()},c.prototype.show=function(){var b=a.Event("show.bs."+this.type);if(this.hasContent()&&this.enabled){this.$element.trigger(b);var d=a.contains(this.$element[0].ownerDocument.documentElement,this.$element[0]);if(b.isDefaultPrevented()||!d)return;var e=this,f=this.tip(),g=this.getUID(this.type);this.setContent(),f.attr("id",g),this.$element.attr("aria-describedby",g),this.options.animation&&f.addClass("fade");var h="function"==typeof this.options.placement?this.options.placement.call(this,f[0],this.$element[0]):this.options.placement,i=/\s?auto?\s?/i,j=i.test(h);j&&(h=h.replace(i,"")||"top"),f.detach().css({top:0,left:0,display:"block"}).addClass(h).data("bs."+this.type,this),this.options.container?f.appendTo(this.options.container):f.insertAfter(this.$element),this.$element.trigger("inserted.bs."+this.type);var k=this.getPosition(),l=f[0].offsetWidth,m=f[0].offsetHeight;if(j){var n=h,o=this.getPosition(this.$viewport);h="bottom"==h&&k.bottom+m>o.bottom?"top":"top"==h&&k.top-mo.width?"left":"left"==h&&k.left-lg.top+g.height&&(e.top=g.top+g.height-i)}else{var j=b.left-f,k=b.left+f+c;jg.right&&(e.left=g.left+g.width-k)}return e},c.prototype.getTitle=function(){var a,b=this.$element,c=this.options;return a=b.attr("data-original-title")||("function"==typeof c.title?c.title.call(b[0]):c.title)},c.prototype.getUID=function(a){do a+=~~(1e6*Math.random());while(document.getElementById(a));return a},c.prototype.tip=function(){if(!this.$tip&&(this.$tip=a(this.options.template),1!=this.$tip.length))throw new Error(this.type+" `template` option must consist of exactly 1 top-level element!");return this.$tip},c.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".tooltip-arrow")},c.prototype.enable=function(){this.enabled=!0},c.prototype.disable=function(){this.enabled=!1},c.prototype.toggleEnabled=function(){this.enabled=!this.enabled},c.prototype.toggle=function(b){var c=this;b&&(c=a(b.currentTarget).data("bs."+this.type),c||(c=new this.constructor(b.currentTarget,this.getDelegateOptions()),a(b.currentTarget).data("bs."+this.type,c))),b?(c.inState.click=!c.inState.click,c.isInStateTrue()?c.enter(c):c.leave(c)):c.tip().hasClass("in")?c.leave(c):c.enter(c)},c.prototype.destroy=function(){var a=this;clearTimeout(this.timeout),this.hide(function(){a.$element.off("."+a.type).removeData("bs."+a.type),a.$tip&&a.$tip.detach(),a.$tip=null,a.$arrow=null,a.$viewport=null,a.$element=null})};var d=a.fn.tooltip;a.fn.tooltip=b,a.fn.tooltip.Constructor=c,a.fn.tooltip.noConflict=function(){return a.fn.tooltip=d,this}}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.popover"),f="object"==typeof b&&b;!e&&/destroy|hide/.test(b)||(e||d.data("bs.popover",e=new c(this,f)),"string"==typeof b&&e[b]())})}var c=function(a,b){this.init("popover",a,b)};if(!a.fn.tooltip)throw new Error("Popover requires tooltip.js");c.VERSION="3.3.7",c.DEFAULTS=a.extend({},a.fn.tooltip.Constructor.DEFAULTS,{placement:"right",trigger:"click",content:"",template:''}),c.prototype=a.extend({},a.fn.tooltip.Constructor.prototype),c.prototype.constructor=c,c.prototype.getDefaults=function(){return c.DEFAULTS},c.prototype.setContent=function(){var a=this.tip(),b=this.getTitle(),c=this.getContent();a.find(".popover-title")[this.options.html?"html":"text"](b),a.find(".popover-content").children().detach().end()[this.options.html?"string"==typeof c?"html":"append":"text"](c),a.removeClass("fade top bottom left right in"),a.find(".popover-title").html()||a.find(".popover-title").hide()},c.prototype.hasContent=function(){return this.getTitle()||this.getContent()},c.prototype.getContent=function(){var a=this.$element,b=this.options;return a.attr("data-content")||("function"==typeof b.content?b.content.call(a[0]):b.content)},c.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".arrow")};var d=a.fn.popover;a.fn.popover=b,a.fn.popover.Constructor=c,a.fn.popover.noConflict=function(){return a.fn.popover=d,this}}(jQuery),+function(a){"use strict";function b(c,d){this.$body=a(document.body),this.$scrollElement=a(a(c).is(document.body)?window:c),this.options=a.extend({},b.DEFAULTS,d),this.selector=(this.options.target||"")+" .nav li > a",this.offsets=[],this.targets=[],this.activeTarget=null,this.scrollHeight=0,this.$scrollElement.on("scroll.bs.scrollspy",a.proxy(this.process,this)),this.refresh(),this.process()}function c(c){return this.each(function(){var d=a(this),e=d.data("bs.scrollspy"),f="object"==typeof c&&c;e||d.data("bs.scrollspy",e=new b(this,f)),"string"==typeof c&&e[c]()})}b.VERSION="3.3.7",b.DEFAULTS={offset:10},b.prototype.getScrollHeight=function(){return this.$scrollElement[0].scrollHeight||Math.max(this.$body[0].scrollHeight,document.documentElement.scrollHeight)},b.prototype.refresh=function(){var b=this,c="offset",d=0;this.offsets=[],this.targets=[],this.scrollHeight=this.getScrollHeight(),a.isWindow(this.$scrollElement[0])||(c="position",d=this.$scrollElement.scrollTop()),this.$body.find(this.selector).map(function(){var b=a(this),e=b.data("target")||b.attr("href"),f=/^#./.test(e)&&a(e);return f&&f.length&&f.is(":visible")&&[[f[c]().top+d,e]]||null}).sort(function(a,b){return a[0]-b[0]}).each(function(){b.offsets.push(this[0]),b.targets.push(this[1])})},b.prototype.process=function(){var a,b=this.$scrollElement.scrollTop()+this.options.offset,c=this.getScrollHeight(),d=this.options.offset+c-this.$scrollElement.height(),e=this.offsets,f=this.targets,g=this.activeTarget;if(this.scrollHeight!=c&&this.refresh(),b>=d)return g!=(a=f[f.length-1])&&this.activate(a);if(g&&b=e[a]&&(void 0===e[a+1]||b .dropdown-menu > .active").removeClass("active").end().find('[data-toggle="tab"]').attr("aria-expanded",!1),b.addClass("active").find('[data-toggle="tab"]').attr("aria-expanded",!0),h?(b[0].offsetWidth,b.addClass("in")):b.removeClass("fade"),b.parent(".dropdown-menu").length&&b.closest("li.dropdown").addClass("active").end().find('[data-toggle="tab"]').attr("aria-expanded",!0),e&&e()}var g=d.find("> .active"),h=e&&a.support.transition&&(g.length&&g.hasClass("fade")||!!d.find("> .fade").length);g.length&&h?g.one("bsTransitionEnd",f).emulateTransitionEnd(c.TRANSITION_DURATION):f(),g.removeClass("in")};var d=a.fn.tab;a.fn.tab=b,a.fn.tab.Constructor=c,a.fn.tab.noConflict=function(){return a.fn.tab=d,this};var e=function(c){c.preventDefault(),b.call(a(this),"show")};a(document).on("click.bs.tab.data-api",'[data-toggle="tab"]',e).on("click.bs.tab.data-api",'[data-toggle="pill"]',e)}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.affix"),f="object"==typeof b&&b;e||d.data("bs.affix",e=new c(this,f)),"string"==typeof b&&e[b]()})}var c=function(b,d){this.options=a.extend({},c.DEFAULTS,d),this.$target=a(this.options.target).on("scroll.bs.affix.data-api",a.proxy(this.checkPosition,this)).on("click.bs.affix.data-api",a.proxy(this.checkPositionWithEventLoop,this)),this.$element=a(b),this.affixed=null,this.unpin=null,this.pinnedOffset=null,this.checkPosition()};c.VERSION="3.3.7",c.RESET="affix affix-top affix-bottom",c.DEFAULTS={offset:0,target:window},c.prototype.getState=function(a,b,c,d){var e=this.$target.scrollTop(),f=this.$element.offset(),g=this.$target.height();if(null!=c&&"top"==this.affixed)return e=a-d&&"bottom"},c.prototype.getPinnedOffset=function(){if(this.pinnedOffset)return this.pinnedOffset;this.$element.removeClass(c.RESET).addClass("affix");var a=this.$target.scrollTop(),b=this.$element.offset();return this.pinnedOffset=b.top-a},c.prototype.checkPositionWithEventLoop=function(){setTimeout(a.proxy(this.checkPosition,this),1)},c.prototype.checkPosition=function(){if(this.$element.is(":visible")){var b=this.$element.height(),d=this.options.offset,e=d.top,f=d.bottom,g=Math.max(a(document).height(),a(document.body).height());"object"!=typeof d&&(f=e=d),"function"==typeof e&&(e=d.top(this.$element)),"function"==typeof f&&(f=d.bottom(this.$element));var h=this.getState(g,b,e,f);if(this.affixed!=h){null!=this.unpin&&this.$element.css("top","");var i="affix"+(h?"-"+h:""),j=a.Event(i+".bs.affix");if(this.$element.trigger(j),j.isDefaultPrevented())return;this.affixed=h,this.unpin="bottom"==h?this.getPinnedOffset():null,this.$element.removeClass(c.RESET).addClass(i).trigger(i.replace("affix","affixed")+".bs.affix")}"bottom"==h&&this.$element.offset({top:g-b-f})}};var d=a.fn.affix;a.fn.affix=b,a.fn.affix.Constructor=c,a.fn.affix.noConflict=function(){return a.fn.affix=d,this},a(window).on("load",function(){a('[data-spy="affix"]').each(function(){var c=a(this),d=c.data();d.offset=d.offset||{},null!=d.offsetBottom&&(d.offset.bottom=d.offsetBottom),null!=d.offsetTop&&(d.offset.top=d.offsetTop),b.call(c,d)})})}(jQuery);!function(){function n(n){return n&&(n.ownerDocument||n.document||n).documentElement}function t(n){return n&&(n.ownerDocument&&n.ownerDocument.defaultView||n.document&&n||n.defaultView)}function e(n,t){return t>n?-1:n>t?1:n>=t?0:NaN}function r(n){return null===n?NaN:+n}function i(n){return!isNaN(n)}function u(n){return{left:function(t,e,r,i){for(arguments.length<3&&(r=0),arguments.length<4&&(i=t.length);i>r;){var u=r+i>>>1;n(t[u],e)<0?r=u+1:i=u}return r},right:function(t,e,r,i){for(arguments.length<3&&(r=0),arguments.length<4&&(i=t.length);i>r;){var u=r+i>>>1;n(t[u],e)>0?i=u:r=u+1}return r}}}function o(n){return n.length}function a(n){for(var t=1;n*t%1;)t*=10;return t}function l(n,t){for(var e in t)Object.defineProperty(n.prototype,e,{value:t[e],enumerable:!1})}function c(){this._=Object.create(null)}function f(n){return(n+="")===bo||n[0]===_o?_o+n:n}function s(n){return(n+="")[0]===_o?n.slice(1):n}function h(n){return f(n)in this._}function p(n){return(n=f(n))in this._&&delete this._[n]}function g(){var n=[];for(var t in this._)n.push(s(t));return n}function v(){var n=0;for(var t in this._)++n;return n}function d(){for(var n in this._)return!1;return!0}function y(){this._=Object.create(null)}function m(n){return n}function M(n,t,e){return function(){var r=e.apply(t,arguments);return r===t?n:r}}function x(n,t){if(t in n)return t;t=t.charAt(0).toUpperCase()+t.slice(1);for(var e=0,r=wo.length;r>e;++e){var i=wo[e]+t;if(i in n)return i}}function b(){}function _(){}function w(n){function t(){for(var t,r=e,i=-1,u=r.length;++ie;e++)for(var i,u=n[e],o=0,a=u.length;a>o;o++)(i=u[o])&&t(i,o,e);return n}function Z(n){return ko(n,qo),n}function V(n){var t,e;return function(r,i,u){var o,a=n[u].update,l=a.length;for(u!=e&&(e=u,t=0),i>=t&&(t=i+1);!(o=a[t])&&++t0&&(n=n.slice(0,a));var c=To.get(n);return c&&(n=c,l=B),a?t?i:r:t?b:u}function $(n,t){return function(e){var r=ao.event;ao.event=e,t[0]=this.__data__;try{n.apply(this,t)}finally{ao.event=r}}}function B(n,t){var e=$(n,t);return function(n){var t=this,r=n.relatedTarget;r&&(r===t||8&r.compareDocumentPosition(t))||e.call(t,n)}}function W(e){var r=".dragsuppress-"+ ++Do,i="click"+r,u=ao.select(t(e)).on("touchmove"+r,S).on("dragstart"+r,S).on("selectstart"+r,S);if(null==Ro&&(Ro="onselectstart"in e?!1:x(e.style,"userSelect")),Ro){var o=n(e).style,a=o[Ro];o[Ro]="none"}return function(n){if(u.on(r,null),Ro&&(o[Ro]=a),n){var t=function(){u.on(i,null)};u.on(i,function(){S(),t()},!0),setTimeout(t,0)}}}function J(n,e){e.changedTouches&&(e=e.changedTouches[0]);var r=n.ownerSVGElement||n;if(r.createSVGPoint){var i=r.createSVGPoint();if(0>Po){var u=t(n);if(u.scrollX||u.scrollY){r=ao.select("body").append("svg").style({position:"absolute",top:0,left:0,margin:0,padding:0,border:"none"},"important");var o=r[0][0].getScreenCTM();Po=!(o.f||o.e),r.remove()}}return Po?(i.x=e.pageX,i.y=e.pageY):(i.x=e.clientX,i.y=e.clientY),i=i.matrixTransform(n.getScreenCTM().inverse()),[i.x,i.y]}var a=n.getBoundingClientRect();return[e.clientX-a.left-n.clientLeft,e.clientY-a.top-n.clientTop]}function G(){return ao.event.changedTouches[0].identifier}function K(n){return n>0?1:0>n?-1:0}function Q(n,t,e){return(t[0]-n[0])*(e[1]-n[1])-(t[1]-n[1])*(e[0]-n[0])}function nn(n){return n>1?0:-1>n?Fo:Math.acos(n)}function tn(n){return n>1?Io:-1>n?-Io:Math.asin(n)}function en(n){return((n=Math.exp(n))-1/n)/2}function rn(n){return((n=Math.exp(n))+1/n)/2}function un(n){return((n=Math.exp(2*n))-1)/(n+1)}function on(n){return(n=Math.sin(n/2))*n}function an(){}function ln(n,t,e){return this instanceof ln?(this.h=+n,this.s=+t,void(this.l=+e)):arguments.length<2?n instanceof ln?new ln(n.h,n.s,n.l):_n(""+n,wn,ln):new ln(n,t,e)}function cn(n,t,e){function r(n){return n>360?n-=360:0>n&&(n+=360),60>n?u+(o-u)*n/60:180>n?o:240>n?u+(o-u)*(240-n)/60:u}function i(n){return Math.round(255*r(n))}var u,o;return n=isNaN(n)?0:(n%=360)<0?n+360:n,t=isNaN(t)?0:0>t?0:t>1?1:t,e=0>e?0:e>1?1:e,o=.5>=e?e*(1+t):e+t-e*t,u=2*e-o,new mn(i(n+120),i(n),i(n-120))}function fn(n,t,e){return this instanceof fn?(this.h=+n,this.c=+t,void(this.l=+e)):arguments.length<2?n instanceof fn?new fn(n.h,n.c,n.l):n instanceof hn?gn(n.l,n.a,n.b):gn((n=Sn((n=ao.rgb(n)).r,n.g,n.b)).l,n.a,n.b):new fn(n,t,e)}function sn(n,t,e){return isNaN(n)&&(n=0),isNaN(t)&&(t=0),new hn(e,Math.cos(n*=Yo)*t,Math.sin(n)*t)}function hn(n,t,e){return this instanceof hn?(this.l=+n,this.a=+t,void(this.b=+e)):arguments.length<2?n instanceof hn?new hn(n.l,n.a,n.b):n instanceof fn?sn(n.h,n.c,n.l):Sn((n=mn(n)).r,n.g,n.b):new hn(n,t,e)}function pn(n,t,e){var r=(n+16)/116,i=r+t/500,u=r-e/200;return i=vn(i)*na,r=vn(r)*ta,u=vn(u)*ea,new mn(yn(3.2404542*i-1.5371385*r-.4985314*u),yn(-.969266*i+1.8760108*r+.041556*u),yn(.0556434*i-.2040259*r+1.0572252*u))}function gn(n,t,e){return n>0?new fn(Math.atan2(e,t)*Zo,Math.sqrt(t*t+e*e),n):new fn(NaN,NaN,n)}function vn(n){return n>.206893034?n*n*n:(n-4/29)/7.787037}function dn(n){return n>.008856?Math.pow(n,1/3):7.787037*n+4/29}function yn(n){return Math.round(255*(.00304>=n?12.92*n:1.055*Math.pow(n,1/2.4)-.055))}function mn(n,t,e){return this instanceof mn?(this.r=~~n,this.g=~~t,void(this.b=~~e)):arguments.length<2?n instanceof mn?new mn(n.r,n.g,n.b):_n(""+n,mn,cn):new mn(n,t,e)}function Mn(n){return new mn(n>>16,n>>8&255,255&n)}function xn(n){return Mn(n)+""}function bn(n){return 16>n?"0"+Math.max(0,n).toString(16):Math.min(255,n).toString(16)}function _n(n,t,e){var r,i,u,o=0,a=0,l=0;if(r=/([a-z]+)\((.*)\)/.exec(n=n.toLowerCase()))switch(i=r[2].split(","),r[1]){case"hsl":return e(parseFloat(i[0]),parseFloat(i[1])/100,parseFloat(i[2])/100);case"rgb":return t(Nn(i[0]),Nn(i[1]),Nn(i[2]))}return(u=ua.get(n))?t(u.r,u.g,u.b):(null==n||"#"!==n.charAt(0)||isNaN(u=parseInt(n.slice(1),16))||(4===n.length?(o=(3840&u)>>4,o=o>>4|o,a=240&u,a=a>>4|a,l=15&u,l=l<<4|l):7===n.length&&(o=(16711680&u)>>16,a=(65280&u)>>8,l=255&u)),t(o,a,l))}function wn(n,t,e){var r,i,u=Math.min(n/=255,t/=255,e/=255),o=Math.max(n,t,e),a=o-u,l=(o+u)/2;return a?(i=.5>l?a/(o+u):a/(2-o-u),r=n==o?(t-e)/a+(e>t?6:0):t==o?(e-n)/a+2:(n-t)/a+4,r*=60):(r=NaN,i=l>0&&1>l?0:r),new ln(r,i,l)}function Sn(n,t,e){n=kn(n),t=kn(t),e=kn(e);var r=dn((.4124564*n+.3575761*t+.1804375*e)/na),i=dn((.2126729*n+.7151522*t+.072175*e)/ta),u=dn((.0193339*n+.119192*t+.9503041*e)/ea);return hn(116*i-16,500*(r-i),200*(i-u))}function kn(n){return(n/=255)<=.04045?n/12.92:Math.pow((n+.055)/1.055,2.4)}function Nn(n){var t=parseFloat(n);return"%"===n.charAt(n.length-1)?Math.round(2.55*t):t}function En(n){return"function"==typeof n?n:function(){return n}}function An(n){return function(t,e,r){return 2===arguments.length&&"function"==typeof e&&(r=e,e=null),Cn(t,e,n,r)}}function Cn(n,t,e,r){function i(){var n,t=l.status;if(!t&&Ln(l)||t>=200&&300>t||304===t){try{n=e.call(u,l)}catch(r){return void o.error.call(u,r)}o.load.call(u,n)}else o.error.call(u,l)}var u={},o=ao.dispatch("beforesend","progress","load","error"),a={},l=new XMLHttpRequest,c=null;return!this.XDomainRequest||"withCredentials"in l||!/^(http(s)?:)?\/\//.test(n)||(l=new XDomainRequest),"onload"in l?l.onload=l.onerror=i:l.onreadystatechange=function(){l.readyState>3&&i()},l.onprogress=function(n){var t=ao.event;ao.event=n;try{o.progress.call(u,l)}finally{ao.event=t}},u.header=function(n,t){return n=(n+"").toLowerCase(),arguments.length<2?a[n]:(null==t?delete a[n]:a[n]=t+"",u)},u.mimeType=function(n){return arguments.length?(t=null==n?null:n+"",u):t},u.responseType=function(n){return arguments.length?(c=n,u):c},u.response=function(n){return e=n,u},["get","post"].forEach(function(n){u[n]=function(){return u.send.apply(u,[n].concat(co(arguments)))}}),u.send=function(e,r,i){if(2===arguments.length&&"function"==typeof r&&(i=r,r=null),l.open(e,n,!0),null==t||"accept"in a||(a.accept=t+",*/*"),l.setRequestHeader)for(var f in a)l.setRequestHeader(f,a[f]);return null!=t&&l.overrideMimeType&&l.overrideMimeType(t),null!=c&&(l.responseType=c),null!=i&&u.on("error",i).on("load",function(n){i(null,n)}),o.beforesend.call(u,l),l.send(null==r?null:r),u},u.abort=function(){return l.abort(),u},ao.rebind(u,o,"on"),null==r?u:u.get(zn(r))}function zn(n){return 1===n.length?function(t,e){n(null==t?e:null)}:n}function Ln(n){var t=n.responseType;return t&&"text"!==t?n.response:n.responseText}function qn(n,t,e){var r=arguments.length;2>r&&(t=0),3>r&&(e=Date.now());var i=e+t,u={c:n,t:i,n:null};return aa?aa.n=u:oa=u,aa=u,la||(ca=clearTimeout(ca),la=1,fa(Tn)),u}function Tn(){var n=Rn(),t=Dn()-n;t>24?(isFinite(t)&&(clearTimeout(ca),ca=setTimeout(Tn,t)),la=0):(la=1,fa(Tn))}function Rn(){for(var n=Date.now(),t=oa;t;)n>=t.t&&t.c(n-t.t)&&(t.c=null),t=t.n;return n}function Dn(){for(var n,t=oa,e=1/0;t;)t.c?(t.t8?function(n){return n/e}:function(n){return n*e},symbol:n}}function jn(n){var t=n.decimal,e=n.thousands,r=n.grouping,i=n.currency,u=r&&e?function(n,t){for(var i=n.length,u=[],o=0,a=r[0],l=0;i>0&&a>0&&(l+a+1>t&&(a=Math.max(1,t-l)),u.push(n.substring(i-=a,i+a)),!((l+=a+1)>t));)a=r[o=(o+1)%r.length];return u.reverse().join(e)}:m;return function(n){var e=ha.exec(n),r=e[1]||" ",o=e[2]||">",a=e[3]||"-",l=e[4]||"",c=e[5],f=+e[6],s=e[7],h=e[8],p=e[9],g=1,v="",d="",y=!1,m=!0;switch(h&&(h=+h.substring(1)),(c||"0"===r&&"="===o)&&(c=r="0",o="="),p){case"n":s=!0,p="g";break;case"%":g=100,d="%",p="f";break;case"p":g=100,d="%",p="r";break;case"b":case"o":case"x":case"X":"#"===l&&(v="0"+p.toLowerCase());case"c":m=!1;case"d":y=!0,h=0;break;case"s":g=-1,p="r"}"$"===l&&(v=i[0],d=i[1]),"r"!=p||h||(p="g"),null!=h&&("g"==p?h=Math.max(1,Math.min(21,h)):"e"!=p&&"f"!=p||(h=Math.max(0,Math.min(20,h)))),p=pa.get(p)||Fn;var M=c&&s;return function(n){var e=d;if(y&&n%1)return"";var i=0>n||0===n&&0>1/n?(n=-n,"-"):"-"===a?"":a;if(0>g){var l=ao.formatPrefix(n,h);n=l.scale(n),e=l.symbol+d}else n*=g;n=p(n,h);var x,b,_=n.lastIndexOf(".");if(0>_){var w=m?n.lastIndexOf("e"):-1;0>w?(x=n,b=""):(x=n.substring(0,w),b=n.substring(w))}else x=n.substring(0,_),b=t+n.substring(_+1);!c&&s&&(x=u(x,1/0));var S=v.length+x.length+b.length+(M?0:i.length),k=f>S?new Array(S=f-S+1).join(r):"";return M&&(x=u(k+x,k.length?f-b.length:1/0)),i+=v,n=x+b,("<"===o?i+n+k:">"===o?k+i+n:"^"===o?k.substring(0,S>>=1)+i+n+k.substring(S):i+(M?n:k+n))+e}}}function Fn(n){return n+""}function Hn(){this._=new Date(arguments.length>1?Date.UTC.apply(this,arguments):arguments[0])}function On(n,t,e){function r(t){var e=n(t),r=u(e,1);return r-t>t-e?e:r}function i(e){return t(e=n(new va(e-1)),1),e}function u(n,e){return t(n=new va(+n),e),n}function o(n,r,u){var o=i(n),a=[];if(u>1)for(;r>o;)e(o)%u||a.push(new Date(+o)),t(o,1);else for(;r>o;)a.push(new Date(+o)),t(o,1);return a}function a(n,t,e){try{va=Hn;var r=new Hn;return r._=n,o(r,t,e)}finally{va=Date}}n.floor=n,n.round=r,n.ceil=i,n.offset=u,n.range=o;var l=n.utc=In(n);return l.floor=l,l.round=In(r),l.ceil=In(i),l.offset=In(u),l.range=a,n}function In(n){return function(t,e){try{va=Hn;var r=new Hn;return r._=t,n(r,e)._}finally{va=Date}}}function Yn(n){function t(n){function t(t){for(var e,i,u,o=[],a=-1,l=0;++aa;){if(r>=c)return-1;if(i=t.charCodeAt(a++),37===i){if(o=t.charAt(a++),u=C[o in ya?t.charAt(a++):o],!u||(r=u(n,e,r))<0)return-1}else if(i!=e.charCodeAt(r++))return-1}return r}function r(n,t,e){_.lastIndex=0;var r=_.exec(t.slice(e));return r?(n.w=w.get(r[0].toLowerCase()),e+r[0].length):-1}function i(n,t,e){x.lastIndex=0;var r=x.exec(t.slice(e));return r?(n.w=b.get(r[0].toLowerCase()),e+r[0].length):-1}function u(n,t,e){N.lastIndex=0;var r=N.exec(t.slice(e));return r?(n.m=E.get(r[0].toLowerCase()),e+r[0].length):-1}function o(n,t,e){S.lastIndex=0;var r=S.exec(t.slice(e));return r?(n.m=k.get(r[0].toLowerCase()),e+r[0].length):-1}function a(n,t,r){return e(n,A.c.toString(),t,r)}function l(n,t,r){return e(n,A.x.toString(),t,r)}function c(n,t,r){return e(n,A.X.toString(),t,r)}function f(n,t,e){var r=M.get(t.slice(e,e+=2).toLowerCase());return null==r?-1:(n.p=r,e)}var s=n.dateTime,h=n.date,p=n.time,g=n.periods,v=n.days,d=n.shortDays,y=n.months,m=n.shortMonths;t.utc=function(n){function e(n){try{va=Hn;var t=new va;return t._=n,r(t)}finally{va=Date}}var r=t(n);return e.parse=function(n){try{va=Hn;var t=r.parse(n);return t&&t._}finally{va=Date}},e.toString=r.toString,e},t.multi=t.utc.multi=ct;var M=ao.map(),x=Vn(v),b=Xn(v),_=Vn(d),w=Xn(d),S=Vn(y),k=Xn(y),N=Vn(m),E=Xn(m);g.forEach(function(n,t){M.set(n.toLowerCase(),t)});var A={a:function(n){return d[n.getDay()]},A:function(n){return v[n.getDay()]},b:function(n){return m[n.getMonth()]},B:function(n){return y[n.getMonth()]},c:t(s),d:function(n,t){return Zn(n.getDate(),t,2)},e:function(n,t){return Zn(n.getDate(),t,2)},H:function(n,t){return Zn(n.getHours(),t,2)},I:function(n,t){return Zn(n.getHours()%12||12,t,2)},j:function(n,t){return Zn(1+ga.dayOfYear(n),t,3)},L:function(n,t){return Zn(n.getMilliseconds(),t,3)},m:function(n,t){return Zn(n.getMonth()+1,t,2)},M:function(n,t){return Zn(n.getMinutes(),t,2)},p:function(n){return g[+(n.getHours()>=12)]},S:function(n,t){return Zn(n.getSeconds(),t,2)},U:function(n,t){return Zn(ga.sundayOfYear(n),t,2)},w:function(n){return n.getDay()},W:function(n,t){return Zn(ga.mondayOfYear(n),t,2)},x:t(h),X:t(p),y:function(n,t){return Zn(n.getFullYear()%100,t,2)},Y:function(n,t){return Zn(n.getFullYear()%1e4,t,4)},Z:at,"%":function(){return"%"}},C={a:r,A:i,b:u,B:o,c:a,d:tt,e:tt,H:rt,I:rt,j:et,L:ot,m:nt,M:it,p:f,S:ut,U:Bn,w:$n,W:Wn,x:l,X:c,y:Gn,Y:Jn,Z:Kn,"%":lt};return t}function Zn(n,t,e){var r=0>n?"-":"",i=(r?-n:n)+"",u=i.length;return r+(e>u?new Array(e-u+1).join(t)+i:i)}function Vn(n){return new RegExp("^(?:"+n.map(ao.requote).join("|")+")","i")}function Xn(n){for(var t=new c,e=-1,r=n.length;++e68?1900:2e3)}function nt(n,t,e){ma.lastIndex=0;var r=ma.exec(t.slice(e,e+2));return r?(n.m=r[0]-1,e+r[0].length):-1}function tt(n,t,e){ma.lastIndex=0;var r=ma.exec(t.slice(e,e+2));return r?(n.d=+r[0],e+r[0].length):-1}function et(n,t,e){ma.lastIndex=0;var r=ma.exec(t.slice(e,e+3));return r?(n.j=+r[0],e+r[0].length):-1}function rt(n,t,e){ma.lastIndex=0;var r=ma.exec(t.slice(e,e+2));return r?(n.H=+r[0],e+r[0].length):-1}function it(n,t,e){ma.lastIndex=0;var r=ma.exec(t.slice(e,e+2));return r?(n.M=+r[0],e+r[0].length):-1}function ut(n,t,e){ma.lastIndex=0;var r=ma.exec(t.slice(e,e+2));return r?(n.S=+r[0],e+r[0].length):-1}function ot(n,t,e){ma.lastIndex=0;var r=ma.exec(t.slice(e,e+3));return r?(n.L=+r[0],e+r[0].length):-1}function at(n){var t=n.getTimezoneOffset(),e=t>0?"-":"+",r=xo(t)/60|0,i=xo(t)%60;return e+Zn(r,"0",2)+Zn(i,"0",2)}function lt(n,t,e){Ma.lastIndex=0;var r=Ma.exec(t.slice(e,e+1));return r?e+r[0].length:-1}function ct(n){for(var t=n.length,e=-1;++e=0?1:-1,a=o*e,l=Math.cos(t),c=Math.sin(t),f=u*c,s=i*l+f*Math.cos(a),h=f*o*Math.sin(a);ka.add(Math.atan2(h,s)),r=n,i=l,u=c}var t,e,r,i,u;Na.point=function(o,a){Na.point=n,r=(t=o)*Yo,i=Math.cos(a=(e=a)*Yo/2+Fo/4),u=Math.sin(a)},Na.lineEnd=function(){n(t,e)}}function dt(n){var t=n[0],e=n[1],r=Math.cos(e);return[r*Math.cos(t),r*Math.sin(t),Math.sin(e)]}function yt(n,t){return n[0]*t[0]+n[1]*t[1]+n[2]*t[2]}function mt(n,t){return[n[1]*t[2]-n[2]*t[1],n[2]*t[0]-n[0]*t[2],n[0]*t[1]-n[1]*t[0]]}function Mt(n,t){n[0]+=t[0],n[1]+=t[1],n[2]+=t[2]}function xt(n,t){return[n[0]*t,n[1]*t,n[2]*t]}function bt(n){var t=Math.sqrt(n[0]*n[0]+n[1]*n[1]+n[2]*n[2]);n[0]/=t,n[1]/=t,n[2]/=t}function _t(n){return[Math.atan2(n[1],n[0]),tn(n[2])]}function wt(n,t){return xo(n[0]-t[0])a;++a)i.point((e=n[a])[0],e[1]);return void i.lineEnd()}var l=new Tt(e,n,null,!0),c=new Tt(e,null,l,!1);l.o=c,u.push(l),o.push(c),l=new Tt(r,n,null,!1),c=new Tt(r,null,l,!0),l.o=c,u.push(l),o.push(c)}}),o.sort(t),qt(u),qt(o),u.length){for(var a=0,l=e,c=o.length;c>a;++a)o[a].e=l=!l;for(var f,s,h=u[0];;){for(var p=h,g=!0;p.v;)if((p=p.n)===h)return;f=p.z,i.lineStart();do{if(p.v=p.o.v=!0,p.e){if(g)for(var a=0,c=f.length;c>a;++a)i.point((s=f[a])[0],s[1]);else r(p.x,p.n.x,1,i);p=p.n}else{if(g){f=p.p.z;for(var a=f.length-1;a>=0;--a)i.point((s=f[a])[0],s[1])}else r(p.x,p.p.x,-1,i);p=p.p}p=p.o,f=p.z,g=!g}while(!p.v);i.lineEnd()}}}function qt(n){if(t=n.length){for(var t,e,r=0,i=n[0];++r0){for(b||(u.polygonStart(),b=!0),u.lineStart();++o1&&2&t&&e.push(e.pop().concat(e.shift())),p.push(e.filter(Dt))}var p,g,v,d=t(u),y=i.invert(r[0],r[1]),m={point:o,lineStart:l,lineEnd:c,polygonStart:function(){m.point=f,m.lineStart=s,m.lineEnd=h,p=[],g=[]},polygonEnd:function(){m.point=o,m.lineStart=l,m.lineEnd=c,p=ao.merge(p);var n=Ot(y,g);p.length?(b||(u.polygonStart(),b=!0),Lt(p,Ut,n,e,u)):n&&(b||(u.polygonStart(),b=!0),u.lineStart(),e(null,null,1,u),u.lineEnd()),b&&(u.polygonEnd(),b=!1),p=g=null},sphere:function(){u.polygonStart(),u.lineStart(),e(null,null,1,u),u.lineEnd(),u.polygonEnd()}},M=Pt(),x=t(M),b=!1;return m}}function Dt(n){return n.length>1}function Pt(){var n,t=[];return{lineStart:function(){t.push(n=[])},point:function(t,e){n.push([t,e])},lineEnd:b,buffer:function(){var e=t;return t=[],n=null,e},rejoin:function(){t.length>1&&t.push(t.pop().concat(t.shift()))}}}function Ut(n,t){return((n=n.x)[0]<0?n[1]-Io-Uo:Io-n[1])-((t=t.x)[0]<0?t[1]-Io-Uo:Io-t[1])}function jt(n){var t,e=NaN,r=NaN,i=NaN;return{lineStart:function(){n.lineStart(),t=1},point:function(u,o){var a=u>0?Fo:-Fo,l=xo(u-e);xo(l-Fo)0?Io:-Io),n.point(i,r),n.lineEnd(),n.lineStart(),n.point(a,r),n.point(u,r),t=0):i!==a&&l>=Fo&&(xo(e-i)Uo?Math.atan((Math.sin(t)*(u=Math.cos(r))*Math.sin(e)-Math.sin(r)*(i=Math.cos(t))*Math.sin(n))/(i*u*o)):(t+r)/2}function Ht(n,t,e,r){var i;if(null==n)i=e*Io,r.point(-Fo,i),r.point(0,i),r.point(Fo,i),r.point(Fo,0),r.point(Fo,-i),r.point(0,-i),r.point(-Fo,-i),r.point(-Fo,0),r.point(-Fo,i);else if(xo(n[0]-t[0])>Uo){var u=n[0]a;++a){var c=t[a],f=c.length;if(f)for(var s=c[0],h=s[0],p=s[1]/2+Fo/4,g=Math.sin(p),v=Math.cos(p),d=1;;){d===f&&(d=0),n=c[d];var y=n[0],m=n[1]/2+Fo/4,M=Math.sin(m),x=Math.cos(m),b=y-h,_=b>=0?1:-1,w=_*b,S=w>Fo,k=g*M;if(ka.add(Math.atan2(k*_*Math.sin(w),v*x+k*Math.cos(w))),u+=S?b+_*Ho:b,S^h>=e^y>=e){var N=mt(dt(s),dt(n));bt(N);var E=mt(i,N);bt(E);var A=(S^b>=0?-1:1)*tn(E[2]);(r>A||r===A&&(N[0]||N[1]))&&(o+=S^b>=0?1:-1)}if(!d++)break;h=y,g=M,v=x,s=n}}return(-Uo>u||Uo>u&&-Uo>ka)^1&o}function It(n){function t(n,t){return Math.cos(n)*Math.cos(t)>u}function e(n){var e,u,l,c,f;return{lineStart:function(){c=l=!1,f=1},point:function(s,h){var p,g=[s,h],v=t(s,h),d=o?v?0:i(s,h):v?i(s+(0>s?Fo:-Fo),h):0;if(!e&&(c=l=v)&&n.lineStart(),v!==l&&(p=r(e,g),(wt(e,p)||wt(g,p))&&(g[0]+=Uo,g[1]+=Uo,v=t(g[0],g[1]))),v!==l)f=0,v?(n.lineStart(),p=r(g,e),n.point(p[0],p[1])):(p=r(e,g),n.point(p[0],p[1]),n.lineEnd()),e=p;else if(a&&e&&o^v){var y;d&u||!(y=r(g,e,!0))||(f=0,o?(n.lineStart(),n.point(y[0][0],y[0][1]),n.point(y[1][0],y[1][1]),n.lineEnd()):(n.point(y[1][0],y[1][1]),n.lineEnd(),n.lineStart(),n.point(y[0][0],y[0][1])))}!v||e&&wt(e,g)||n.point(g[0],g[1]),e=g,l=v,u=d},lineEnd:function(){l&&n.lineEnd(),e=null},clean:function(){return f|(c&&l)<<1}}}function r(n,t,e){var r=dt(n),i=dt(t),o=[1,0,0],a=mt(r,i),l=yt(a,a),c=a[0],f=l-c*c;if(!f)return!e&&n;var s=u*l/f,h=-u*c/f,p=mt(o,a),g=xt(o,s),v=xt(a,h);Mt(g,v);var d=p,y=yt(g,d),m=yt(d,d),M=y*y-m*(yt(g,g)-1);if(!(0>M)){var x=Math.sqrt(M),b=xt(d,(-y-x)/m);if(Mt(b,g),b=_t(b),!e)return b;var _,w=n[0],S=t[0],k=n[1],N=t[1];w>S&&(_=w,w=S,S=_);var E=S-w,A=xo(E-Fo)E;if(!A&&k>N&&(_=k,k=N,N=_),C?A?k+N>0^b[1]<(xo(b[0]-w)Fo^(w<=b[0]&&b[0]<=S)){var z=xt(d,(-y+x)/m);return Mt(z,g),[b,_t(z)]}}}function i(t,e){var r=o?n:Fo-n,i=0;return-r>t?i|=1:t>r&&(i|=2),-r>e?i|=4:e>r&&(i|=8),i}var u=Math.cos(n),o=u>0,a=xo(u)>Uo,l=ve(n,6*Yo);return Rt(t,e,l,o?[0,-n]:[-Fo,n-Fo])}function Yt(n,t,e,r){return function(i){var u,o=i.a,a=i.b,l=o.x,c=o.y,f=a.x,s=a.y,h=0,p=1,g=f-l,v=s-c;if(u=n-l,g||!(u>0)){if(u/=g,0>g){if(h>u)return;p>u&&(p=u)}else if(g>0){if(u>p)return;u>h&&(h=u)}if(u=e-l,g||!(0>u)){if(u/=g,0>g){if(u>p)return;u>h&&(h=u)}else if(g>0){if(h>u)return;p>u&&(p=u)}if(u=t-c,v||!(u>0)){if(u/=v,0>v){if(h>u)return;p>u&&(p=u)}else if(v>0){if(u>p)return;u>h&&(h=u)}if(u=r-c,v||!(0>u)){if(u/=v,0>v){if(u>p)return;u>h&&(h=u)}else if(v>0){if(h>u)return;p>u&&(p=u)}return h>0&&(i.a={x:l+h*g,y:c+h*v}),1>p&&(i.b={x:l+p*g,y:c+p*v}),i}}}}}}function Zt(n,t,e,r){function i(r,i){return xo(r[0]-n)0?0:3:xo(r[0]-e)0?2:1:xo(r[1]-t)0?1:0:i>0?3:2}function u(n,t){return o(n.x,t.x)}function o(n,t){var e=i(n,1),r=i(t,1);return e!==r?e-r:0===e?t[1]-n[1]:1===e?n[0]-t[0]:2===e?n[1]-t[1]:t[0]-n[0]}return function(a){function l(n){for(var t=0,e=d.length,r=n[1],i=0;e>i;++i)for(var u,o=1,a=d[i],l=a.length,c=a[0];l>o;++o)u=a[o],c[1]<=r?u[1]>r&&Q(c,u,n)>0&&++t:u[1]<=r&&Q(c,u,n)<0&&--t,c=u;return 0!==t}function c(u,a,l,c){var f=0,s=0;if(null==u||(f=i(u,l))!==(s=i(a,l))||o(u,a)<0^l>0){do c.point(0===f||3===f?n:e,f>1?r:t);while((f=(f+l+4)%4)!==s)}else c.point(a[0],a[1])}function f(i,u){return i>=n&&e>=i&&u>=t&&r>=u}function s(n,t){f(n,t)&&a.point(n,t)}function h(){C.point=g,d&&d.push(y=[]),S=!0,w=!1,b=_=NaN}function p(){v&&(g(m,M),x&&w&&E.rejoin(),v.push(E.buffer())),C.point=s,w&&a.lineEnd()}function g(n,t){n=Math.max(-Ha,Math.min(Ha,n)),t=Math.max(-Ha,Math.min(Ha,t));var e=f(n,t);if(d&&y.push([n,t]),S)m=n,M=t,x=e,S=!1,e&&(a.lineStart(),a.point(n,t));else if(e&&w)a.point(n,t);else{var r={a:{x:b,y:_},b:{x:n,y:t}};A(r)?(w||(a.lineStart(),a.point(r.a.x,r.a.y)),a.point(r.b.x,r.b.y),e||a.lineEnd(),k=!1):e&&(a.lineStart(),a.point(n,t),k=!1)}b=n,_=t,w=e}var v,d,y,m,M,x,b,_,w,S,k,N=a,E=Pt(),A=Yt(n,t,e,r),C={point:s,lineStart:h,lineEnd:p,polygonStart:function(){a=E,v=[],d=[],k=!0},polygonEnd:function(){a=N,v=ao.merge(v);var t=l([n,r]),e=k&&t,i=v.length;(e||i)&&(a.polygonStart(),e&&(a.lineStart(),c(null,null,1,a),a.lineEnd()),i&&Lt(v,u,t,c,a),a.polygonEnd()),v=d=y=null}};return C}}function Vt(n){var t=0,e=Fo/3,r=ae(n),i=r(t,e);return i.parallels=function(n){return arguments.length?r(t=n[0]*Fo/180,e=n[1]*Fo/180):[t/Fo*180,e/Fo*180]},i}function Xt(n,t){function e(n,t){var e=Math.sqrt(u-2*i*Math.sin(t))/i;return[e*Math.sin(n*=i),o-e*Math.cos(n)]}var r=Math.sin(n),i=(r+Math.sin(t))/2,u=1+r*(2*i-r),o=Math.sqrt(u)/i;return e.invert=function(n,t){var e=o-t;return[Math.atan2(n,e)/i,tn((u-(n*n+e*e)*i*i)/(2*i))]},e}function $t(){function n(n,t){Ia+=i*n-r*t,r=n,i=t}var t,e,r,i;$a.point=function(u,o){$a.point=n,t=r=u,e=i=o},$a.lineEnd=function(){n(t,e)}}function Bt(n,t){Ya>n&&(Ya=n),n>Va&&(Va=n),Za>t&&(Za=t),t>Xa&&(Xa=t)}function Wt(){function n(n,t){o.push("M",n,",",t,u)}function t(n,t){o.push("M",n,",",t),a.point=e}function e(n,t){o.push("L",n,",",t)}function r(){a.point=n}function i(){o.push("Z")}var u=Jt(4.5),o=[],a={point:n,lineStart:function(){a.point=t},lineEnd:r,polygonStart:function(){a.lineEnd=i},polygonEnd:function(){a.lineEnd=r,a.point=n},pointRadius:function(n){return u=Jt(n),a},result:function(){if(o.length){var n=o.join("");return o=[],n}}};return a}function Jt(n){return"m0,"+n+"a"+n+","+n+" 0 1,1 0,"+-2*n+"a"+n+","+n+" 0 1,1 0,"+2*n+"z"}function Gt(n,t){Ca+=n,za+=t,++La}function Kt(){function n(n,r){var i=n-t,u=r-e,o=Math.sqrt(i*i+u*u);qa+=o*(t+n)/2,Ta+=o*(e+r)/2,Ra+=o,Gt(t=n,e=r)}var t,e;Wa.point=function(r,i){Wa.point=n,Gt(t=r,e=i)}}function Qt(){Wa.point=Gt}function ne(){function n(n,t){var e=n-r,u=t-i,o=Math.sqrt(e*e+u*u);qa+=o*(r+n)/2,Ta+=o*(i+t)/2,Ra+=o,o=i*n-r*t,Da+=o*(r+n),Pa+=o*(i+t),Ua+=3*o,Gt(r=n,i=t)}var t,e,r,i;Wa.point=function(u,o){Wa.point=n,Gt(t=r=u,e=i=o)},Wa.lineEnd=function(){n(t,e)}}function te(n){function t(t,e){n.moveTo(t+o,e),n.arc(t,e,o,0,Ho)}function e(t,e){n.moveTo(t,e),a.point=r}function r(t,e){n.lineTo(t,e)}function i(){a.point=t}function u(){n.closePath()}var o=4.5,a={point:t,lineStart:function(){a.point=e},lineEnd:i,polygonStart:function(){a.lineEnd=u},polygonEnd:function(){a.lineEnd=i,a.point=t},pointRadius:function(n){return o=n,a},result:b};return a}function ee(n){function t(n){return(a?r:e)(n)}function e(t){return ue(t,function(e,r){e=n(e,r),t.point(e[0],e[1])})}function r(t){function e(e,r){e=n(e,r),t.point(e[0],e[1])}function r(){M=NaN,S.point=u,t.lineStart()}function u(e,r){var u=dt([e,r]),o=n(e,r);i(M,x,m,b,_,w,M=o[0],x=o[1],m=e,b=u[0],_=u[1],w=u[2],a,t),t.point(M,x)}function o(){S.point=e,t.lineEnd()}function l(){ r(),S.point=c,S.lineEnd=f}function c(n,t){u(s=n,h=t),p=M,g=x,v=b,d=_,y=w,S.point=u}function f(){i(M,x,m,b,_,w,p,g,s,v,d,y,a,t),S.lineEnd=o,o()}var s,h,p,g,v,d,y,m,M,x,b,_,w,S={point:e,lineStart:r,lineEnd:o,polygonStart:function(){t.polygonStart(),S.lineStart=l},polygonEnd:function(){t.polygonEnd(),S.lineStart=r}};return S}function i(t,e,r,a,l,c,f,s,h,p,g,v,d,y){var m=f-t,M=s-e,x=m*m+M*M;if(x>4*u&&d--){var b=a+p,_=l+g,w=c+v,S=Math.sqrt(b*b+_*_+w*w),k=Math.asin(w/=S),N=xo(xo(w)-1)u||xo((m*z+M*L)/x-.5)>.3||o>a*p+l*g+c*v)&&(i(t,e,r,a,l,c,A,C,N,b/=S,_/=S,w,d,y),y.point(A,C),i(A,C,N,b,_,w,f,s,h,p,g,v,d,y))}}var u=.5,o=Math.cos(30*Yo),a=16;return t.precision=function(n){return arguments.length?(a=(u=n*n)>0&&16,t):Math.sqrt(u)},t}function re(n){var t=ee(function(t,e){return n([t*Zo,e*Zo])});return function(n){return le(t(n))}}function ie(n){this.stream=n}function ue(n,t){return{point:t,sphere:function(){n.sphere()},lineStart:function(){n.lineStart()},lineEnd:function(){n.lineEnd()},polygonStart:function(){n.polygonStart()},polygonEnd:function(){n.polygonEnd()}}}function oe(n){return ae(function(){return n})()}function ae(n){function t(n){return n=a(n[0]*Yo,n[1]*Yo),[n[0]*h+l,c-n[1]*h]}function e(n){return n=a.invert((n[0]-l)/h,(c-n[1])/h),n&&[n[0]*Zo,n[1]*Zo]}function r(){a=Ct(o=se(y,M,x),u);var n=u(v,d);return l=p-n[0]*h,c=g+n[1]*h,i()}function i(){return f&&(f.valid=!1,f=null),t}var u,o,a,l,c,f,s=ee(function(n,t){return n=u(n,t),[n[0]*h+l,c-n[1]*h]}),h=150,p=480,g=250,v=0,d=0,y=0,M=0,x=0,b=Fa,_=m,w=null,S=null;return t.stream=function(n){return f&&(f.valid=!1),f=le(b(o,s(_(n)))),f.valid=!0,f},t.clipAngle=function(n){return arguments.length?(b=null==n?(w=n,Fa):It((w=+n)*Yo),i()):w},t.clipExtent=function(n){return arguments.length?(S=n,_=n?Zt(n[0][0],n[0][1],n[1][0],n[1][1]):m,i()):S},t.scale=function(n){return arguments.length?(h=+n,r()):h},t.translate=function(n){return arguments.length?(p=+n[0],g=+n[1],r()):[p,g]},t.center=function(n){return arguments.length?(v=n[0]%360*Yo,d=n[1]%360*Yo,r()):[v*Zo,d*Zo]},t.rotate=function(n){return arguments.length?(y=n[0]%360*Yo,M=n[1]%360*Yo,x=n.length>2?n[2]%360*Yo:0,r()):[y*Zo,M*Zo,x*Zo]},ao.rebind(t,s,"precision"),function(){return u=n.apply(this,arguments),t.invert=u.invert&&e,r()}}function le(n){return ue(n,function(t,e){n.point(t*Yo,e*Yo)})}function ce(n,t){return[n,t]}function fe(n,t){return[n>Fo?n-Ho:-Fo>n?n+Ho:n,t]}function se(n,t,e){return n?t||e?Ct(pe(n),ge(t,e)):pe(n):t||e?ge(t,e):fe}function he(n){return function(t,e){return t+=n,[t>Fo?t-Ho:-Fo>t?t+Ho:t,e]}}function pe(n){var t=he(n);return t.invert=he(-n),t}function ge(n,t){function e(n,t){var e=Math.cos(t),a=Math.cos(n)*e,l=Math.sin(n)*e,c=Math.sin(t),f=c*r+a*i;return[Math.atan2(l*u-f*o,a*r-c*i),tn(f*u+l*o)]}var r=Math.cos(n),i=Math.sin(n),u=Math.cos(t),o=Math.sin(t);return e.invert=function(n,t){var e=Math.cos(t),a=Math.cos(n)*e,l=Math.sin(n)*e,c=Math.sin(t),f=c*u-l*o;return[Math.atan2(l*u+c*o,a*r+f*i),tn(f*r-a*i)]},e}function ve(n,t){var e=Math.cos(n),r=Math.sin(n);return function(i,u,o,a){var l=o*t;null!=i?(i=de(e,i),u=de(e,u),(o>0?u>i:i>u)&&(i+=o*Ho)):(i=n+o*Ho,u=n-.5*l);for(var c,f=i;o>0?f>u:u>f;f-=l)a.point((c=_t([e,-r*Math.cos(f),-r*Math.sin(f)]))[0],c[1])}}function de(n,t){var e=dt(t);e[0]-=n,bt(e);var r=nn(-e[1]);return((-e[2]<0?-r:r)+2*Math.PI-Uo)%(2*Math.PI)}function ye(n,t,e){var r=ao.range(n,t-Uo,e).concat(t);return function(n){return r.map(function(t){return[n,t]})}}function me(n,t,e){var r=ao.range(n,t-Uo,e).concat(t);return function(n){return r.map(function(t){return[t,n]})}}function Me(n){return n.source}function xe(n){return n.target}function be(n,t,e,r){var i=Math.cos(t),u=Math.sin(t),o=Math.cos(r),a=Math.sin(r),l=i*Math.cos(n),c=i*Math.sin(n),f=o*Math.cos(e),s=o*Math.sin(e),h=2*Math.asin(Math.sqrt(on(r-t)+i*o*on(e-n))),p=1/Math.sin(h),g=h?function(n){var t=Math.sin(n*=h)*p,e=Math.sin(h-n)*p,r=e*l+t*f,i=e*c+t*s,o=e*u+t*a;return[Math.atan2(i,r)*Zo,Math.atan2(o,Math.sqrt(r*r+i*i))*Zo]}:function(){return[n*Zo,t*Zo]};return g.distance=h,g}function _e(){function n(n,i){var u=Math.sin(i*=Yo),o=Math.cos(i),a=xo((n*=Yo)-t),l=Math.cos(a);Ja+=Math.atan2(Math.sqrt((a=o*Math.sin(a))*a+(a=r*u-e*o*l)*a),e*u+r*o*l),t=n,e=u,r=o}var t,e,r;Ga.point=function(i,u){t=i*Yo,e=Math.sin(u*=Yo),r=Math.cos(u),Ga.point=n},Ga.lineEnd=function(){Ga.point=Ga.lineEnd=b}}function we(n,t){function e(t,e){var r=Math.cos(t),i=Math.cos(e),u=n(r*i);return[u*i*Math.sin(t),u*Math.sin(e)]}return e.invert=function(n,e){var r=Math.sqrt(n*n+e*e),i=t(r),u=Math.sin(i),o=Math.cos(i);return[Math.atan2(n*u,r*o),Math.asin(r&&e*u/r)]},e}function Se(n,t){function e(n,t){o>0?-Io+Uo>t&&(t=-Io+Uo):t>Io-Uo&&(t=Io-Uo);var e=o/Math.pow(i(t),u);return[e*Math.sin(u*n),o-e*Math.cos(u*n)]}var r=Math.cos(n),i=function(n){return Math.tan(Fo/4+n/2)},u=n===t?Math.sin(n):Math.log(r/Math.cos(t))/Math.log(i(t)/i(n)),o=r*Math.pow(i(n),u)/u;return u?(e.invert=function(n,t){var e=o-t,r=K(u)*Math.sqrt(n*n+e*e);return[Math.atan2(n,e)/u,2*Math.atan(Math.pow(o/r,1/u))-Io]},e):Ne}function ke(n,t){function e(n,t){var e=u-t;return[e*Math.sin(i*n),u-e*Math.cos(i*n)]}var r=Math.cos(n),i=n===t?Math.sin(n):(r-Math.cos(t))/(t-n),u=r/i+n;return xo(i)i;i++){for(;r>1&&Q(n[e[r-2]],n[e[r-1]],n[i])<=0;)--r;e[r++]=i}return e.slice(0,r)}function qe(n,t){return n[0]-t[0]||n[1]-t[1]}function Te(n,t,e){return(e[0]-t[0])*(n[1]-t[1])<(e[1]-t[1])*(n[0]-t[0])}function Re(n,t,e,r){var i=n[0],u=e[0],o=t[0]-i,a=r[0]-u,l=n[1],c=e[1],f=t[1]-l,s=r[1]-c,h=(a*(l-c)-s*(i-u))/(s*o-a*f);return[i+h*o,l+h*f]}function De(n){var t=n[0],e=n[n.length-1];return!(t[0]-e[0]||t[1]-e[1])}function Pe(){rr(this),this.edge=this.site=this.circle=null}function Ue(n){var t=cl.pop()||new Pe;return t.site=n,t}function je(n){Be(n),ol.remove(n),cl.push(n),rr(n)}function Fe(n){var t=n.circle,e=t.x,r=t.cy,i={x:e,y:r},u=n.P,o=n.N,a=[n];je(n);for(var l=u;l.circle&&xo(e-l.circle.x)f;++f)c=a[f],l=a[f-1],nr(c.edge,l.site,c.site,i);l=a[0],c=a[s-1],c.edge=Ke(l.site,c.site,null,i),$e(l),$e(c)}function He(n){for(var t,e,r,i,u=n.x,o=n.y,a=ol._;a;)if(r=Oe(a,o)-u,r>Uo)a=a.L;else{if(i=u-Ie(a,o),!(i>Uo)){r>-Uo?(t=a.P,e=a):i>-Uo?(t=a,e=a.N):t=e=a;break}if(!a.R){t=a;break}a=a.R}var l=Ue(n);if(ol.insert(t,l),t||e){if(t===e)return Be(t),e=Ue(t.site),ol.insert(l,e),l.edge=e.edge=Ke(t.site,l.site),$e(t),void $e(e);if(!e)return void(l.edge=Ke(t.site,l.site));Be(t),Be(e);var c=t.site,f=c.x,s=c.y,h=n.x-f,p=n.y-s,g=e.site,v=g.x-f,d=g.y-s,y=2*(h*d-p*v),m=h*h+p*p,M=v*v+d*d,x={x:(d*m-p*M)/y+f,y:(h*M-v*m)/y+s};nr(e.edge,c,g,x),l.edge=Ke(c,n,null,x),e.edge=Ke(n,g,null,x),$e(t),$e(e)}}function Oe(n,t){var e=n.site,r=e.x,i=e.y,u=i-t;if(!u)return r;var o=n.P;if(!o)return-(1/0);e=o.site;var a=e.x,l=e.y,c=l-t;if(!c)return a;var f=a-r,s=1/u-1/c,h=f/c;return s?(-h+Math.sqrt(h*h-2*s*(f*f/(-2*c)-l+c/2+i-u/2)))/s+r:(r+a)/2}function Ie(n,t){var e=n.N;if(e)return Oe(e,t);var r=n.site;return r.y===t?r.x:1/0}function Ye(n){this.site=n,this.edges=[]}function Ze(n){for(var t,e,r,i,u,o,a,l,c,f,s=n[0][0],h=n[1][0],p=n[0][1],g=n[1][1],v=ul,d=v.length;d--;)if(u=v[d],u&&u.prepare())for(a=u.edges,l=a.length,o=0;l>o;)f=a[o].end(),r=f.x,i=f.y,c=a[++o%l].start(),t=c.x,e=c.y,(xo(r-t)>Uo||xo(i-e)>Uo)&&(a.splice(o,0,new tr(Qe(u.site,f,xo(r-s)Uo?{x:s,y:xo(t-s)Uo?{x:xo(e-g)Uo?{x:h,y:xo(t-h)Uo?{x:xo(e-p)=-jo)){var p=l*l+c*c,g=f*f+s*s,v=(s*p-c*g)/h,d=(l*g-f*p)/h,s=d+a,y=fl.pop()||new Xe;y.arc=n,y.site=i,y.x=v+o,y.y=s+Math.sqrt(v*v+d*d),y.cy=s,n.circle=y;for(var m=null,M=ll._;M;)if(y.yd||d>=a)return;if(h>g){if(u){if(u.y>=c)return}else u={x:d,y:l};e={x:d,y:c}}else{if(u){if(u.yr||r>1)if(h>g){if(u){if(u.y>=c)return}else u={x:(l-i)/r,y:l};e={x:(c-i)/r,y:c}}else{if(u){if(u.yp){if(u){if(u.x>=a)return}else u={x:o,y:r*o+i};e={x:a,y:r*a+i}}else{if(u){if(u.xu||s>o||r>h||i>p)){if(g=n.point){var g,v=t-n.x,d=e-n.y,y=v*v+d*d;if(l>y){var m=Math.sqrt(l=y);r=t-m,i=e-m,u=t+m,o=e+m,a=g}}for(var M=n.nodes,x=.5*(f+h),b=.5*(s+p),_=t>=x,w=e>=b,S=w<<1|_,k=S+4;k>S;++S)if(n=M[3&S])switch(3&S){case 0:c(n,f,s,x,b);break;case 1:c(n,x,s,h,b);break;case 2:c(n,f,b,x,p);break;case 3:c(n,x,b,h,p)}}}(n,r,i,u,o),a}function vr(n,t){n=ao.rgb(n),t=ao.rgb(t);var e=n.r,r=n.g,i=n.b,u=t.r-e,o=t.g-r,a=t.b-i;return function(n){return"#"+bn(Math.round(e+u*n))+bn(Math.round(r+o*n))+bn(Math.round(i+a*n))}}function dr(n,t){var e,r={},i={};for(e in n)e in t?r[e]=Mr(n[e],t[e]):i[e]=n[e];for(e in t)e in n||(i[e]=t[e]);return function(n){for(e in r)i[e]=r[e](n);return i}}function yr(n,t){return n=+n,t=+t,function(e){return n*(1-e)+t*e}}function mr(n,t){var e,r,i,u=hl.lastIndex=pl.lastIndex=0,o=-1,a=[],l=[];for(n+="",t+="";(e=hl.exec(n))&&(r=pl.exec(t));)(i=r.index)>u&&(i=t.slice(u,i),a[o]?a[o]+=i:a[++o]=i),(e=e[0])===(r=r[0])?a[o]?a[o]+=r:a[++o]=r:(a[++o]=null,l.push({i:o,x:yr(e,r)})),u=pl.lastIndex;return ur;++r)a[(e=l[r]).i]=e.x(n);return a.join("")})}function Mr(n,t){for(var e,r=ao.interpolators.length;--r>=0&&!(e=ao.interpolators[r](n,t)););return e}function xr(n,t){var e,r=[],i=[],u=n.length,o=t.length,a=Math.min(n.length,t.length);for(e=0;a>e;++e)r.push(Mr(n[e],t[e]));for(;u>e;++e)i[e]=n[e];for(;o>e;++e)i[e]=t[e];return function(n){for(e=0;a>e;++e)i[e]=r[e](n);return i}}function br(n){return function(t){return 0>=t?0:t>=1?1:n(t)}}function _r(n){return function(t){return 1-n(1-t)}}function wr(n){return function(t){return.5*(.5>t?n(2*t):2-n(2-2*t))}}function Sr(n){return n*n}function kr(n){return n*n*n}function Nr(n){if(0>=n)return 0;if(n>=1)return 1;var t=n*n,e=t*n;return 4*(.5>n?e:3*(n-t)+e-.75)}function Er(n){return function(t){return Math.pow(t,n)}}function Ar(n){return 1-Math.cos(n*Io)}function Cr(n){return Math.pow(2,10*(n-1))}function zr(n){return 1-Math.sqrt(1-n*n)}function Lr(n,t){var e;return arguments.length<2&&(t=.45),arguments.length?e=t/Ho*Math.asin(1/n):(n=1,e=t/4),function(r){return 1+n*Math.pow(2,-10*r)*Math.sin((r-e)*Ho/t)}}function qr(n){return n||(n=1.70158),function(t){return t*t*((n+1)*t-n)}}function Tr(n){return 1/2.75>n?7.5625*n*n:2/2.75>n?7.5625*(n-=1.5/2.75)*n+.75:2.5/2.75>n?7.5625*(n-=2.25/2.75)*n+.9375:7.5625*(n-=2.625/2.75)*n+.984375}function Rr(n,t){n=ao.hcl(n),t=ao.hcl(t);var e=n.h,r=n.c,i=n.l,u=t.h-e,o=t.c-r,a=t.l-i;return isNaN(o)&&(o=0,r=isNaN(r)?t.c:r),isNaN(u)?(u=0,e=isNaN(e)?t.h:e):u>180?u-=360:-180>u&&(u+=360),function(n){return sn(e+u*n,r+o*n,i+a*n)+""}}function Dr(n,t){n=ao.hsl(n),t=ao.hsl(t);var e=n.h,r=n.s,i=n.l,u=t.h-e,o=t.s-r,a=t.l-i;return isNaN(o)&&(o=0,r=isNaN(r)?t.s:r),isNaN(u)?(u=0,e=isNaN(e)?t.h:e):u>180?u-=360:-180>u&&(u+=360),function(n){return cn(e+u*n,r+o*n,i+a*n)+""}}function Pr(n,t){n=ao.lab(n),t=ao.lab(t);var e=n.l,r=n.a,i=n.b,u=t.l-e,o=t.a-r,a=t.b-i;return function(n){return pn(e+u*n,r+o*n,i+a*n)+""}}function Ur(n,t){return t-=n,function(e){return Math.round(n+t*e)}}function jr(n){var t=[n.a,n.b],e=[n.c,n.d],r=Hr(t),i=Fr(t,e),u=Hr(Or(e,t,-i))||0;t[0]*e[1]180?t+=360:t-n>180&&(n+=360),r.push({i:e.push(Ir(e)+"rotate(",null,")")-2,x:yr(n,t)})):t&&e.push(Ir(e)+"rotate("+t+")")}function Vr(n,t,e,r){n!==t?r.push({i:e.push(Ir(e)+"skewX(",null,")")-2,x:yr(n,t)}):t&&e.push(Ir(e)+"skewX("+t+")")}function Xr(n,t,e,r){if(n[0]!==t[0]||n[1]!==t[1]){var i=e.push(Ir(e)+"scale(",null,",",null,")");r.push({i:i-4,x:yr(n[0],t[0])},{i:i-2,x:yr(n[1],t[1])})}else 1===t[0]&&1===t[1]||e.push(Ir(e)+"scale("+t+")")}function $r(n,t){var e=[],r=[];return n=ao.transform(n),t=ao.transform(t),Yr(n.translate,t.translate,e,r),Zr(n.rotate,t.rotate,e,r),Vr(n.skew,t.skew,e,r),Xr(n.scale,t.scale,e,r),n=t=null,function(n){for(var t,i=-1,u=r.length;++i=0;)e.push(i[r])}function oi(n,t){for(var e=[n],r=[];null!=(n=e.pop());)if(r.push(n),(u=n.children)&&(i=u.length))for(var i,u,o=-1;++oe;++e)(t=n[e][1])>i&&(r=e,i=t);return r}function yi(n){return n.reduce(mi,0)}function mi(n,t){return n+t[1]}function Mi(n,t){return xi(n,Math.ceil(Math.log(t.length)/Math.LN2+1))}function xi(n,t){for(var e=-1,r=+n[0],i=(n[1]-r)/t,u=[];++e<=t;)u[e]=i*e+r;return u}function bi(n){return[ao.min(n),ao.max(n)]}function _i(n,t){return n.value-t.value}function wi(n,t){var e=n._pack_next;n._pack_next=t,t._pack_prev=n,t._pack_next=e,e._pack_prev=t}function Si(n,t){n._pack_next=t,t._pack_prev=n}function ki(n,t){var e=t.x-n.x,r=t.y-n.y,i=n.r+t.r;return.999*i*i>e*e+r*r}function Ni(n){function t(n){f=Math.min(n.x-n.r,f),s=Math.max(n.x+n.r,s),h=Math.min(n.y-n.r,h),p=Math.max(n.y+n.r,p)}if((e=n.children)&&(c=e.length)){var e,r,i,u,o,a,l,c,f=1/0,s=-(1/0),h=1/0,p=-(1/0);if(e.forEach(Ei),r=e[0],r.x=-r.r,r.y=0,t(r),c>1&&(i=e[1],i.x=i.r,i.y=0,t(i),c>2))for(u=e[2],zi(r,i,u),t(u),wi(r,u),r._pack_prev=u,wi(u,i),i=r._pack_next,o=3;c>o;o++){zi(r,i,u=e[o]);var g=0,v=1,d=1;for(a=i._pack_next;a!==i;a=a._pack_next,v++)if(ki(a,u)){g=1;break}if(1==g)for(l=r._pack_prev;l!==a._pack_prev&&!ki(l,u);l=l._pack_prev,d++);g?(d>v||v==d&&i.ro;o++)u=e[o],u.x-=y,u.y-=m,M=Math.max(M,u.r+Math.sqrt(u.x*u.x+u.y*u.y));n.r=M,e.forEach(Ai)}}function Ei(n){n._pack_next=n._pack_prev=n}function Ai(n){delete n._pack_next,delete n._pack_prev}function Ci(n,t,e,r){var i=n.children;if(n.x=t+=r*n.x,n.y=e+=r*n.y,n.r*=r,i)for(var u=-1,o=i.length;++u=0;)t=i[u],t.z+=e,t.m+=e,e+=t.s+(r+=t.c)}function Pi(n,t,e){return n.a.parent===t.parent?n.a:e}function Ui(n){return 1+ao.max(n,function(n){return n.y})}function ji(n){return n.reduce(function(n,t){return n+t.x},0)/n.length}function Fi(n){var t=n.children;return t&&t.length?Fi(t[0]):n}function Hi(n){var t,e=n.children;return e&&(t=e.length)?Hi(e[t-1]):n}function Oi(n){return{x:n.x,y:n.y,dx:n.dx,dy:n.dy}}function Ii(n,t){var e=n.x+t[3],r=n.y+t[0],i=n.dx-t[1]-t[3],u=n.dy-t[0]-t[2];return 0>i&&(e+=i/2,i=0),0>u&&(r+=u/2,u=0),{x:e,y:r,dx:i,dy:u}}function Yi(n){var t=n[0],e=n[n.length-1];return e>t?[t,e]:[e,t]}function Zi(n){return n.rangeExtent?n.rangeExtent():Yi(n.range())}function Vi(n,t,e,r){var i=e(n[0],n[1]),u=r(t[0],t[1]);return function(n){return u(i(n))}}function Xi(n,t){var e,r=0,i=n.length-1,u=n[r],o=n[i];return u>o&&(e=r,r=i,i=e,e=u,u=o,o=e),n[r]=t.floor(u),n[i]=t.ceil(o),n}function $i(n){return n?{floor:function(t){return Math.floor(t/n)*n},ceil:function(t){return Math.ceil(t/n)*n}}:Sl}function Bi(n,t,e,r){var i=[],u=[],o=0,a=Math.min(n.length,t.length)-1;for(n[a]2?Bi:Vi,l=r?Wr:Br;return o=i(n,t,l,e),a=i(t,n,l,Mr),u}function u(n){return o(n)}var o,a;return u.invert=function(n){return a(n)},u.domain=function(t){return arguments.length?(n=t.map(Number),i()):n},u.range=function(n){return arguments.length?(t=n,i()):t},u.rangeRound=function(n){return u.range(n).interpolate(Ur)},u.clamp=function(n){return arguments.length?(r=n,i()):r},u.interpolate=function(n){return arguments.length?(e=n,i()):e},u.ticks=function(t){return Qi(n,t)},u.tickFormat=function(t,e){return nu(n,t,e)},u.nice=function(t){return Gi(n,t),i()},u.copy=function(){return Wi(n,t,e,r)},i()}function Ji(n,t){return ao.rebind(n,t,"range","rangeRound","interpolate","clamp")}function Gi(n,t){return Xi(n,$i(Ki(n,t)[2])),Xi(n,$i(Ki(n,t)[2])),n}function Ki(n,t){null==t&&(t=10);var e=Yi(n),r=e[1]-e[0],i=Math.pow(10,Math.floor(Math.log(r/t)/Math.LN10)),u=t/r*i;return.15>=u?i*=10:.35>=u?i*=5:.75>=u&&(i*=2),e[0]=Math.ceil(e[0]/i)*i,e[1]=Math.floor(e[1]/i)*i+.5*i,e[2]=i,e}function Qi(n,t){return ao.range.apply(ao,Ki(n,t))}function nu(n,t,e){var r=Ki(n,t);if(e){var i=ha.exec(e);if(i.shift(),"s"===i[8]){var u=ao.formatPrefix(Math.max(xo(r[0]),xo(r[1])));return i[7]||(i[7]="."+tu(u.scale(r[2]))),i[8]="f",e=ao.format(i.join("")),function(n){return e(u.scale(n))+u.symbol}}i[7]||(i[7]="."+eu(i[8],r)),e=i.join("")}else e=",."+tu(r[2])+"f";return ao.format(e)}function tu(n){return-Math.floor(Math.log(n)/Math.LN10+.01)}function eu(n,t){var e=tu(t[2]);return n in kl?Math.abs(e-tu(Math.max(xo(t[0]),xo(t[1]))))+ +("e"!==n):e-2*("%"===n)}function ru(n,t,e,r){function i(n){return(e?Math.log(0>n?0:n):-Math.log(n>0?0:-n))/Math.log(t)}function u(n){return e?Math.pow(t,n):-Math.pow(t,-n)}function o(t){return n(i(t))}return o.invert=function(t){return u(n.invert(t))},o.domain=function(t){return arguments.length?(e=t[0]>=0,n.domain((r=t.map(Number)).map(i)),o):r},o.base=function(e){return arguments.length?(t=+e,n.domain(r.map(i)),o):t},o.nice=function(){var t=Xi(r.map(i),e?Math:El);return n.domain(t),r=t.map(u),o},o.ticks=function(){var n=Yi(r),o=[],a=n[0],l=n[1],c=Math.floor(i(a)),f=Math.ceil(i(l)),s=t%1?2:t;if(isFinite(f-c)){if(e){for(;f>c;c++)for(var h=1;s>h;h++)o.push(u(c)*h);o.push(u(c))}else for(o.push(u(c));c++0;h--)o.push(u(c)*h);for(c=0;o[c]l;f--);o=o.slice(c,f)}return o},o.tickFormat=function(n,e){if(!arguments.length)return Nl;arguments.length<2?e=Nl:"function"!=typeof e&&(e=ao.format(e));var r=Math.max(1,t*n/o.ticks().length);return function(n){var o=n/u(Math.round(i(n)));return t-.5>o*t&&(o*=t),r>=o?e(n):""}},o.copy=function(){return ru(n.copy(),t,e,r)},Ji(o,n)}function iu(n,t,e){function r(t){return n(i(t))}var i=uu(t),u=uu(1/t);return r.invert=function(t){return u(n.invert(t))},r.domain=function(t){return arguments.length?(n.domain((e=t.map(Number)).map(i)),r):e},r.ticks=function(n){return Qi(e,n)},r.tickFormat=function(n,t){return nu(e,n,t)},r.nice=function(n){return r.domain(Gi(e,n))},r.exponent=function(o){return arguments.length?(i=uu(t=o),u=uu(1/t),n.domain(e.map(i)),r):t},r.copy=function(){return iu(n.copy(),t,e)},Ji(r,n)}function uu(n){return function(t){return 0>t?-Math.pow(-t,n):Math.pow(t,n)}}function ou(n,t){function e(e){return u[((i.get(e)||("range"===t.t?i.set(e,n.push(e)):NaN))-1)%u.length]}function r(t,e){return ao.range(n.length).map(function(n){return t+e*n})}var i,u,o;return e.domain=function(r){if(!arguments.length)return n;n=[],i=new c;for(var u,o=-1,a=r.length;++oe?[NaN,NaN]:[e>0?a[e-1]:n[0],et?NaN:t/u+n,[t,t+1/u]},r.copy=function(){return lu(n,t,e)},i()}function cu(n,t){function e(e){return e>=e?t[ao.bisect(n,e)]:void 0}return e.domain=function(t){return arguments.length?(n=t,e):n},e.range=function(n){return arguments.length?(t=n,e):t},e.invertExtent=function(e){return e=t.indexOf(e),[n[e-1],n[e]]},e.copy=function(){return cu(n,t)},e}function fu(n){function t(n){return+n}return t.invert=t,t.domain=t.range=function(e){return arguments.length?(n=e.map(t),t):n},t.ticks=function(t){return Qi(n,t)},t.tickFormat=function(t,e){return nu(n,t,e)},t.copy=function(){return fu(n)},t}function su(){return 0}function hu(n){return n.innerRadius}function pu(n){return n.outerRadius}function gu(n){return n.startAngle}function vu(n){return n.endAngle}function du(n){return n&&n.padAngle}function yu(n,t,e,r){return(n-e)*t-(t-r)*n>0?0:1}function mu(n,t,e,r,i){var u=n[0]-t[0],o=n[1]-t[1],a=(i?r:-r)/Math.sqrt(u*u+o*o),l=a*o,c=-a*u,f=n[0]+l,s=n[1]+c,h=t[0]+l,p=t[1]+c,g=(f+h)/2,v=(s+p)/2,d=h-f,y=p-s,m=d*d+y*y,M=e-r,x=f*p-h*s,b=(0>y?-1:1)*Math.sqrt(Math.max(0,M*M*m-x*x)),_=(x*y-d*b)/m,w=(-x*d-y*b)/m,S=(x*y+d*b)/m,k=(-x*d+y*b)/m,N=_-g,E=w-v,A=S-g,C=k-v;return N*N+E*E>A*A+C*C&&(_=S,w=k),[[_-l,w-c],[_*e/M,w*e/M]]}function Mu(n){function t(t){function o(){c.push("M",u(n(f),a))}for(var l,c=[],f=[],s=-1,h=t.length,p=En(e),g=En(r);++s1?n.join("L"):n+"Z"}function bu(n){return n.join("L")+"Z"}function _u(n){for(var t=0,e=n.length,r=n[0],i=[r[0],",",r[1]];++t1&&i.push("H",r[0]),i.join("")}function wu(n){for(var t=0,e=n.length,r=n[0],i=[r[0],",",r[1]];++t1){a=t[1],u=n[l],l++,r+="C"+(i[0]+o[0])+","+(i[1]+o[1])+","+(u[0]-a[0])+","+(u[1]-a[1])+","+u[0]+","+u[1];for(var c=2;c9&&(i=3*t/Math.sqrt(i),o[a]=i*e,o[a+1]=i*r));for(a=-1;++a<=l;)i=(n[Math.min(l,a+1)][0]-n[Math.max(0,a-1)][0])/(6*(1+o[a]*o[a])),u.push([i||0,o[a]*i||0]);return u}function Fu(n){return n.length<3?xu(n):n[0]+Au(n,ju(n))}function Hu(n){for(var t,e,r,i=-1,u=n.length;++i=t?o(n-t):void(f.c=o)}function o(e){var i=g.active,u=g[i];u&&(u.timer.c=null,u.timer.t=NaN,--g.count,delete g[i],u.event&&u.event.interrupt.call(n,n.__data__,u.index));for(var o in g)if(r>+o){var c=g[o];c.timer.c=null,c.timer.t=NaN,--g.count,delete g[o]}f.c=a,qn(function(){return f.c&&a(e||1)&&(f.c=null,f.t=NaN),1},0,l),g.active=r,v.event&&v.event.start.call(n,n.__data__,t),p=[],v.tween.forEach(function(e,r){(r=r.call(n,n.__data__,t))&&p.push(r)}),h=v.ease,s=v.duration}function a(i){for(var u=i/s,o=h(u),a=p.length;a>0;)p[--a].call(n,o);return u>=1?(v.event&&v.event.end.call(n,n.__data__,t),--g.count?delete g[r]:delete n[e],1):void 0}var l,f,s,h,p,g=n[e]||(n[e]={active:0,count:0}),v=g[r];v||(l=i.time,f=qn(u,0,l),v=g[r]={tween:new c,time:l,timer:f,delay:i.delay,duration:i.duration,ease:i.ease,index:t},i=null,++g.count)}function no(n,t,e){n.attr("transform",function(n){var r=t(n);return"translate("+(isFinite(r)?r:e(n))+",0)"})}function to(n,t,e){n.attr("transform",function(n){var r=t(n);return"translate(0,"+(isFinite(r)?r:e(n))+")"})}function eo(n){return n.toISOString()}function ro(n,t,e){function r(t){return n(t)}function i(n,e){var r=n[1]-n[0],i=r/e,u=ao.bisect(Kl,i);return u==Kl.length?[t.year,Ki(n.map(function(n){return n/31536e6}),e)[2]]:u?t[i/Kl[u-1]1?{floor:function(t){for(;e(t=n.floor(t));)t=io(t-1);return t},ceil:function(t){for(;e(t=n.ceil(t));)t=io(+t+1);return t}}:n))},r.ticks=function(n,t){var e=Yi(r.domain()),u=null==n?i(e,10):"number"==typeof n?i(e,n):!n.range&&[{range:n},t];return u&&(n=u[0],t=u[1]),n.range(e[0],io(+e[1]+1),1>t?1:t)},r.tickFormat=function(){return e},r.copy=function(){return ro(n.copy(),t,e)},Ji(r,n)}function io(n){return new Date(n)}function uo(n){return JSON.parse(n.responseText)}function oo(n){var t=fo.createRange();return t.selectNode(fo.body),t.createContextualFragment(n.responseText)}var ao={version:"3.5.17"},lo=[].slice,co=function(n){return lo.call(n)},fo=this.document;if(fo)try{co(fo.documentElement.childNodes)[0].nodeType}catch(so){co=function(n){for(var t=n.length,e=new Array(t);t--;)e[t]=n[t];return e}}if(Date.now||(Date.now=function(){return+new Date}),fo)try{fo.createElement("DIV").style.setProperty("opacity",0,"")}catch(ho){var po=this.Element.prototype,go=po.setAttribute,vo=po.setAttributeNS,yo=this.CSSStyleDeclaration.prototype,mo=yo.setProperty;po.setAttribute=function(n,t){go.call(this,n,t+"")},po.setAttributeNS=function(n,t,e){vo.call(this,n,t,e+"")},yo.setProperty=function(n,t,e){mo.call(this,n,t+"",e)}}ao.ascending=e,ao.descending=function(n,t){return n>t?-1:t>n?1:t>=n?0:NaN},ao.min=function(n,t){var e,r,i=-1,u=n.length;if(1===arguments.length){for(;++i=r){e=r;break}for(;++ir&&(e=r)}else{for(;++i=r){e=r;break}for(;++ir&&(e=r)}return e},ao.max=function(n,t){var e,r,i=-1,u=n.length;if(1===arguments.length){for(;++i=r){e=r;break}for(;++ie&&(e=r)}else{for(;++i=r){e=r;break}for(;++ie&&(e=r)}return e},ao.extent=function(n,t){var e,r,i,u=-1,o=n.length;if(1===arguments.length){for(;++u=r){e=i=r;break}for(;++ur&&(e=r),r>i&&(i=r))}else{for(;++u=r){e=i=r;break}for(;++ur&&(e=r),r>i&&(i=r))}return[e,i]},ao.sum=function(n,t){var e,r=0,u=n.length,o=-1;if(1===arguments.length)for(;++o1?l/(f-1):void 0},ao.deviation=function(){var n=ao.variance.apply(this,arguments);return n?Math.sqrt(n):n};var Mo=u(e);ao.bisectLeft=Mo.left,ao.bisect=ao.bisectRight=Mo.right,ao.bisector=function(n){return u(1===n.length?function(t,r){return e(n(t),r)}:n)},ao.shuffle=function(n,t,e){(u=arguments.length)<3&&(e=n.length,2>u&&(t=0));for(var r,i,u=e-t;u;)i=Math.random()*u--|0,r=n[u+t],n[u+t]=n[i+t],n[i+t]=r;return n},ao.permute=function(n,t){for(var e=t.length,r=new Array(e);e--;)r[e]=n[t[e]];return r},ao.pairs=function(n){for(var t,e=0,r=n.length-1,i=n[0],u=new Array(0>r?0:r);r>e;)u[e]=[t=i,i=n[++e]];return u},ao.transpose=function(n){if(!(i=n.length))return[];for(var t=-1,e=ao.min(n,o),r=new Array(e);++t=0;)for(r=n[i],t=r.length;--t>=0;)e[--o]=r[t];return e};var xo=Math.abs;ao.range=function(n,t,e){if(arguments.length<3&&(e=1,arguments.length<2&&(t=n,n=0)),(t-n)/e===1/0)throw new Error("infinite range");var r,i=[],u=a(xo(e)),o=-1;if(n*=u,t*=u,e*=u,0>e)for(;(r=n+e*++o)>t;)i.push(r/u);else for(;(r=n+e*++o)=u.length)return r?r.call(i,o):e?o.sort(e):o;for(var l,f,s,h,p=-1,g=o.length,v=u[a++],d=new c;++p=u.length)return n;var r=[],i=o[e++];return n.forEach(function(n,i){r.push({key:n,values:t(i,e)})}),i?r.sort(function(n,t){return i(n.key,t.key)}):r}var e,r,i={},u=[],o=[];return i.map=function(t,e){return n(e,t,0)},i.entries=function(e){return t(n(ao.map,e,0),0)},i.key=function(n){return u.push(n),i},i.sortKeys=function(n){return o[u.length-1]=n,i},i.sortValues=function(n){return e=n,i},i.rollup=function(n){return r=n,i},i},ao.set=function(n){var t=new y;if(n)for(var e=0,r=n.length;r>e;++e)t.add(n[e]);return t},l(y,{has:h,add:function(n){return this._[f(n+="")]=!0,n},remove:p,values:g,size:v,empty:d,forEach:function(n){for(var t in this._)n.call(this,s(t))}}),ao.behavior={},ao.rebind=function(n,t){for(var e,r=1,i=arguments.length;++r=0&&(r=n.slice(e+1),n=n.slice(0,e)),n)return arguments.length<2?this[n].on(r):this[n].on(r,t);if(2===arguments.length){if(null==t)for(n in this)this.hasOwnProperty(n)&&this[n].on(r,null);return this}},ao.event=null,ao.requote=function(n){return n.replace(So,"\\$&")};var So=/[\\\^\$\*\+\?\|\[\]\(\)\.\{\}]/g,ko={}.__proto__?function(n,t){n.__proto__=t}:function(n,t){for(var e in t)n[e]=t[e]},No=function(n,t){return t.querySelector(n)},Eo=function(n,t){return t.querySelectorAll(n)},Ao=function(n,t){var e=n.matches||n[x(n,"matchesSelector")];return(Ao=function(n,t){return e.call(n,t)})(n,t)};"function"==typeof Sizzle&&(No=function(n,t){return Sizzle(n,t)[0]||null},Eo=Sizzle,Ao=Sizzle.matchesSelector),ao.selection=function(){return ao.select(fo.documentElement)};var Co=ao.selection.prototype=[];Co.select=function(n){var t,e,r,i,u=[];n=A(n);for(var o=-1,a=this.length;++o=0&&"xmlns"!==(e=n.slice(0,t))&&(n=n.slice(t+1)),Lo.hasOwnProperty(e)?{space:Lo[e],local:n}:n}},Co.attr=function(n,t){if(arguments.length<2){if("string"==typeof n){var e=this.node();return n=ao.ns.qualify(n),n.local?e.getAttributeNS(n.space,n.local):e.getAttribute(n)}for(t in n)this.each(z(t,n[t]));return this}return this.each(z(n,t))},Co.classed=function(n,t){if(arguments.length<2){if("string"==typeof n){var e=this.node(),r=(n=T(n)).length,i=-1;if(t=e.classList){for(;++ii){if("string"!=typeof n){2>i&&(e="");for(r in n)this.each(P(r,n[r],e));return this}if(2>i){var u=this.node();return t(u).getComputedStyle(u,null).getPropertyValue(n)}r=""}return this.each(P(n,e,r))},Co.property=function(n,t){if(arguments.length<2){if("string"==typeof n)return this.node()[n];for(t in n)this.each(U(t,n[t]));return this}return this.each(U(n,t))},Co.text=function(n){return arguments.length?this.each("function"==typeof n?function(){var t=n.apply(this,arguments);this.textContent=null==t?"":t}:null==n?function(){this.textContent=""}:function(){this.textContent=n}):this.node().textContent},Co.html=function(n){return arguments.length?this.each("function"==typeof n?function(){var t=n.apply(this,arguments);this.innerHTML=null==t?"":t}:null==n?function(){this.innerHTML=""}:function(){this.innerHTML=n}):this.node().innerHTML},Co.append=function(n){return n=j(n),this.select(function(){return this.appendChild(n.apply(this,arguments))})},Co.insert=function(n,t){return n=j(n),t=A(t),this.select(function(){return this.insertBefore(n.apply(this,arguments),t.apply(this,arguments)||null)})},Co.remove=function(){return this.each(F)},Co.data=function(n,t){function e(n,e){var r,i,u,o=n.length,s=e.length,h=Math.min(o,s),p=new Array(s),g=new Array(s),v=new Array(o);if(t){var d,y=new c,m=new Array(o);for(r=-1;++rr;++r)g[r]=H(e[r]);for(;o>r;++r)v[r]=n[r]}g.update=p,g.parentNode=p.parentNode=v.parentNode=n.parentNode,a.push(g),l.push(p),f.push(v)}var r,i,u=-1,o=this.length;if(!arguments.length){for(n=new Array(o=(r=this[0]).length);++uu;u++){i.push(t=[]),t.parentNode=(e=this[u]).parentNode;for(var a=0,l=e.length;l>a;a++)(r=e[a])&&n.call(r,r.__data__,a,u)&&t.push(r)}return E(i)},Co.order=function(){for(var n=-1,t=this.length;++n=0;)(e=r[i])&&(u&&u!==e.nextSibling&&u.parentNode.insertBefore(e,u),u=e);return this},Co.sort=function(n){n=I.apply(this,arguments);for(var t=-1,e=this.length;++tn;n++)for(var e=this[n],r=0,i=e.length;i>r;r++){var u=e[r];if(u)return u}return null},Co.size=function(){var n=0;return Y(this,function(){++n}),n};var qo=[];ao.selection.enter=Z,ao.selection.enter.prototype=qo,qo.append=Co.append,qo.empty=Co.empty,qo.node=Co.node,qo.call=Co.call,qo.size=Co.size,qo.select=function(n){for(var t,e,r,i,u,o=[],a=-1,l=this.length;++ar){if("string"!=typeof n){2>r&&(t=!1);for(e in n)this.each(X(e,n[e],t));return this}if(2>r)return(r=this.node()["__on"+n])&&r._;e=!1}return this.each(X(n,t,e))};var To=ao.map({mouseenter:"mouseover",mouseleave:"mouseout"});fo&&To.forEach(function(n){"on"+n in fo&&To.remove(n)});var Ro,Do=0;ao.mouse=function(n){return J(n,k())};var Po=this.navigator&&/WebKit/.test(this.navigator.userAgent)?-1:0;ao.touch=function(n,t,e){if(arguments.length<3&&(e=t,t=k().changedTouches),t)for(var r,i=0,u=t.length;u>i;++i)if((r=t[i]).identifier===e)return J(n,r)},ao.behavior.drag=function(){function n(){this.on("mousedown.drag",u).on("touchstart.drag",o)}function e(n,t,e,u,o){return function(){function a(){var n,e,r=t(h,v);r&&(n=r[0]-M[0],e=r[1]-M[1],g|=n|e,M=r,p({type:"drag",x:r[0]+c[0],y:r[1]+c[1],dx:n,dy:e}))}function l(){t(h,v)&&(y.on(u+d,null).on(o+d,null),m(g),p({type:"dragend"}))}var c,f=this,s=ao.event.target.correspondingElement||ao.event.target,h=f.parentNode,p=r.of(f,arguments),g=0,v=n(),d=".drag"+(null==v?"":"-"+v),y=ao.select(e(s)).on(u+d,a).on(o+d,l),m=W(s),M=t(h,v);i?(c=i.apply(f,arguments),c=[c.x-M[0],c.y-M[1]]):c=[0,0],p({type:"dragstart"})}}var r=N(n,"drag","dragstart","dragend"),i=null,u=e(b,ao.mouse,t,"mousemove","mouseup"),o=e(G,ao.touch,m,"touchmove","touchend");return n.origin=function(t){return arguments.length?(i=t,n):i},ao.rebind(n,r,"on")},ao.touches=function(n,t){return arguments.length<2&&(t=k().touches),t?co(t).map(function(t){var e=J(n,t);return e.identifier=t.identifier,e}):[]};var Uo=1e-6,jo=Uo*Uo,Fo=Math.PI,Ho=2*Fo,Oo=Ho-Uo,Io=Fo/2,Yo=Fo/180,Zo=180/Fo,Vo=Math.SQRT2,Xo=2,$o=4;ao.interpolateZoom=function(n,t){var e,r,i=n[0],u=n[1],o=n[2],a=t[0],l=t[1],c=t[2],f=a-i,s=l-u,h=f*f+s*s;if(jo>h)r=Math.log(c/o)/Vo,e=function(n){return[i+n*f,u+n*s,o*Math.exp(Vo*n*r)]};else{var p=Math.sqrt(h),g=(c*c-o*o+$o*h)/(2*o*Xo*p),v=(c*c-o*o-$o*h)/(2*c*Xo*p),d=Math.log(Math.sqrt(g*g+1)-g),y=Math.log(Math.sqrt(v*v+1)-v);r=(y-d)/Vo,e=function(n){var t=n*r,e=rn(d),a=o/(Xo*p)*(e*un(Vo*t+d)-en(d));return[i+a*f,u+a*s,o*e/rn(Vo*t+d)]}}return e.duration=1e3*r,e},ao.behavior.zoom=function(){function n(n){n.on(L,s).on(Wo+".zoom",p).on("dblclick.zoom",g).on(R,h)}function e(n){return[(n[0]-k.x)/k.k,(n[1]-k.y)/k.k]}function r(n){return[n[0]*k.k+k.x,n[1]*k.k+k.y]}function i(n){k.k=Math.max(A[0],Math.min(A[1],n))}function u(n,t){t=r(t),k.x+=n[0]-t[0],k.y+=n[1]-t[1]}function o(t,e,r,o){t.__chart__={x:k.x,y:k.y,k:k.k},i(Math.pow(2,o)),u(d=e,r),t=ao.select(t),C>0&&(t=t.transition().duration(C)),t.call(n.event)}function a(){b&&b.domain(x.range().map(function(n){return(n-k.x)/k.k}).map(x.invert)),w&&w.domain(_.range().map(function(n){return(n-k.y)/k.k}).map(_.invert))}function l(n){z++||n({type:"zoomstart"})}function c(n){a(),n({type:"zoom",scale:k.k,translate:[k.x,k.y]})}function f(n){--z||(n({type:"zoomend"}),d=null)}function s(){function n(){a=1,u(ao.mouse(i),h),c(o)}function r(){s.on(q,null).on(T,null),p(a),f(o)}var i=this,o=D.of(i,arguments),a=0,s=ao.select(t(i)).on(q,n).on(T,r),h=e(ao.mouse(i)),p=W(i);Il.call(i),l(o)}function h(){function n(){var n=ao.touches(g);return p=k.k,n.forEach(function(n){n.identifier in d&&(d[n.identifier]=e(n))}),n}function t(){var t=ao.event.target;ao.select(t).on(x,r).on(b,a),_.push(t);for(var e=ao.event.changedTouches,i=0,u=e.length;u>i;++i)d[e[i].identifier]=null;var l=n(),c=Date.now();if(1===l.length){if(500>c-M){var f=l[0];o(g,f,d[f.identifier],Math.floor(Math.log(k.k)/Math.LN2)+1),S()}M=c}else if(l.length>1){var f=l[0],s=l[1],h=f[0]-s[0],p=f[1]-s[1];y=h*h+p*p}}function r(){var n,t,e,r,o=ao.touches(g);Il.call(g);for(var a=0,l=o.length;l>a;++a,r=null)if(e=o[a],r=d[e.identifier]){if(t)break;n=e,t=r}if(r){var f=(f=e[0]-n[0])*f+(f=e[1]-n[1])*f,s=y&&Math.sqrt(f/y);n=[(n[0]+e[0])/2,(n[1]+e[1])/2],t=[(t[0]+r[0])/2,(t[1]+r[1])/2],i(s*p)}M=null,u(n,t),c(v)}function a(){if(ao.event.touches.length){for(var t=ao.event.changedTouches,e=0,r=t.length;r>e;++e)delete d[t[e].identifier];for(var i in d)return void n()}ao.selectAll(_).on(m,null),w.on(L,s).on(R,h),N(),f(v)}var p,g=this,v=D.of(g,arguments),d={},y=0,m=".zoom-"+ao.event.changedTouches[0].identifier,x="touchmove"+m,b="touchend"+m,_=[],w=ao.select(g),N=W(g);t(),l(v),w.on(L,null).on(R,t)}function p(){var n=D.of(this,arguments);m?clearTimeout(m):(Il.call(this),v=e(d=y||ao.mouse(this)),l(n)),m=setTimeout(function(){m=null,f(n)},50),S(),i(Math.pow(2,.002*Bo())*k.k),u(d,v),c(n)}function g(){var n=ao.mouse(this),t=Math.log(k.k)/Math.LN2;o(this,n,e(n),ao.event.shiftKey?Math.ceil(t)-1:Math.floor(t)+1)}var v,d,y,m,M,x,b,_,w,k={x:0,y:0,k:1},E=[960,500],A=Jo,C=250,z=0,L="mousedown.zoom",q="mousemove.zoom",T="mouseup.zoom",R="touchstart.zoom",D=N(n,"zoomstart","zoom","zoomend");return Wo||(Wo="onwheel"in fo?(Bo=function(){return-ao.event.deltaY*(ao.event.deltaMode?120:1)},"wheel"):"onmousewheel"in fo?(Bo=function(){return ao.event.wheelDelta},"mousewheel"):(Bo=function(){return-ao.event.detail},"MozMousePixelScroll")),n.event=function(n){n.each(function(){var n=D.of(this,arguments),t=k;Hl?ao.select(this).transition().each("start.zoom",function(){k=this.__chart__||{x:0,y:0,k:1},l(n)}).tween("zoom:zoom",function(){var e=E[0],r=E[1],i=d?d[0]:e/2,u=d?d[1]:r/2,o=ao.interpolateZoom([(i-k.x)/k.k,(u-k.y)/k.k,e/k.k],[(i-t.x)/t.k,(u-t.y)/t.k,e/t.k]);return function(t){var r=o(t),a=e/r[2];this.__chart__=k={x:i-r[0]*a,y:u-r[1]*a,k:a},c(n)}}).each("interrupt.zoom",function(){f(n)}).each("end.zoom",function(){f(n)}):(this.__chart__=k,l(n),c(n),f(n))})},n.translate=function(t){return arguments.length?(k={x:+t[0],y:+t[1],k:k.k},a(),n):[k.x,k.y]},n.scale=function(t){return arguments.length?(k={x:k.x,y:k.y,k:null},i(+t),a(),n):k.k},n.scaleExtent=function(t){return arguments.length?(A=null==t?Jo:[+t[0],+t[1]],n):A},n.center=function(t){return arguments.length?(y=t&&[+t[0],+t[1]],n):y},n.size=function(t){return arguments.length?(E=t&&[+t[0],+t[1]],n):E},n.duration=function(t){return arguments.length?(C=+t,n):C},n.x=function(t){return arguments.length?(b=t,x=t.copy(),k={x:0,y:0,k:1},n):b},n.y=function(t){return arguments.length?(w=t,_=t.copy(),k={x:0,y:0,k:1},n):w},ao.rebind(n,D,"on")};var Bo,Wo,Jo=[0,1/0];ao.color=an,an.prototype.toString=function(){return this.rgb()+""},ao.hsl=ln;var Go=ln.prototype=new an;Go.brighter=function(n){return n=Math.pow(.7,arguments.length?n:1),new ln(this.h,this.s,this.l/n)},Go.darker=function(n){return n=Math.pow(.7,arguments.length?n:1),new ln(this.h,this.s,n*this.l)},Go.rgb=function(){return cn(this.h,this.s,this.l)},ao.hcl=fn;var Ko=fn.prototype=new an;Ko.brighter=function(n){return new fn(this.h,this.c,Math.min(100,this.l+Qo*(arguments.length?n:1)))},Ko.darker=function(n){return new fn(this.h,this.c,Math.max(0,this.l-Qo*(arguments.length?n:1)))},Ko.rgb=function(){return sn(this.h,this.c,this.l).rgb()},ao.lab=hn;var Qo=18,na=.95047,ta=1,ea=1.08883,ra=hn.prototype=new an;ra.brighter=function(n){return new hn(Math.min(100,this.l+Qo*(arguments.length?n:1)),this.a,this.b)},ra.darker=function(n){return new hn(Math.max(0,this.l-Qo*(arguments.length?n:1)),this.a,this.b)},ra.rgb=function(){return pn(this.l,this.a,this.b)},ao.rgb=mn;var ia=mn.prototype=new an;ia.brighter=function(n){n=Math.pow(.7,arguments.length?n:1);var t=this.r,e=this.g,r=this.b,i=30;return t||e||r?(t&&i>t&&(t=i),e&&i>e&&(e=i),r&&i>r&&(r=i),new mn(Math.min(255,t/n),Math.min(255,e/n),Math.min(255,r/n))):new mn(i,i,i)},ia.darker=function(n){return n=Math.pow(.7,arguments.length?n:1),new mn(n*this.r,n*this.g,n*this.b)},ia.hsl=function(){return wn(this.r,this.g,this.b)},ia.toString=function(){return"#"+bn(this.r)+bn(this.g)+bn(this.b)};var ua=ao.map({aliceblue:15792383,antiquewhite:16444375,aqua:65535,aquamarine:8388564,azure:15794175,beige:16119260,bisque:16770244,black:0,blanchedalmond:16772045,blue:255,blueviolet:9055202,brown:10824234,burlywood:14596231,cadetblue:6266528,chartreuse:8388352,chocolate:13789470,coral:16744272,cornflowerblue:6591981,cornsilk:16775388,crimson:14423100,cyan:65535,darkblue:139,darkcyan:35723,darkgoldenrod:12092939,darkgray:11119017,darkgreen:25600,darkgrey:11119017,darkkhaki:12433259,darkmagenta:9109643,darkolivegreen:5597999,darkorange:16747520,darkorchid:10040012,darkred:9109504,darksalmon:15308410,darkseagreen:9419919,darkslateblue:4734347,darkslategray:3100495,darkslategrey:3100495,darkturquoise:52945,darkviolet:9699539,deeppink:16716947,deepskyblue:49151,dimgray:6908265,dimgrey:6908265,dodgerblue:2003199,firebrick:11674146,floralwhite:16775920,forestgreen:2263842,fuchsia:16711935,gainsboro:14474460,ghostwhite:16316671,gold:16766720,goldenrod:14329120,gray:8421504,green:32768,greenyellow:11403055,grey:8421504,honeydew:15794160,hotpink:16738740,indianred:13458524,indigo:4915330,ivory:16777200,khaki:15787660,lavender:15132410,lavenderblush:16773365,lawngreen:8190976,lemonchiffon:16775885,lightblue:11393254,lightcoral:15761536,lightcyan:14745599,lightgoldenrodyellow:16448210,lightgray:13882323,lightgreen:9498256,lightgrey:13882323,lightpink:16758465,lightsalmon:16752762,lightseagreen:2142890,lightskyblue:8900346,lightslategray:7833753,lightslategrey:7833753,lightsteelblue:11584734,lightyellow:16777184,lime:65280,limegreen:3329330,linen:16445670,magenta:16711935,maroon:8388608,mediumaquamarine:6737322,mediumblue:205,mediumorchid:12211667,mediumpurple:9662683,mediumseagreen:3978097,mediumslateblue:8087790,mediumspringgreen:64154,mediumturquoise:4772300,mediumvioletred:13047173,midnightblue:1644912,mintcream:16121850,mistyrose:16770273,moccasin:16770229,navajowhite:16768685,navy:128,oldlace:16643558,olive:8421376,olivedrab:7048739,orange:16753920,orangered:16729344,orchid:14315734,palegoldenrod:15657130,palegreen:10025880,paleturquoise:11529966,palevioletred:14381203,papayawhip:16773077,peachpuff:16767673,peru:13468991,pink:16761035,plum:14524637,powderblue:11591910,purple:8388736,rebeccapurple:6697881,red:16711680,rosybrown:12357519,royalblue:4286945,saddlebrown:9127187,salmon:16416882,sandybrown:16032864,seagreen:3050327,seashell:16774638,sienna:10506797,silver:12632256,skyblue:8900331,slateblue:6970061,slategray:7372944,slategrey:7372944,snow:16775930,springgreen:65407,steelblue:4620980,tan:13808780,teal:32896,thistle:14204888,tomato:16737095,turquoise:4251856,violet:15631086,wheat:16113331,white:16777215,whitesmoke:16119285,yellow:16776960,yellowgreen:10145074});ua.forEach(function(n,t){ua.set(n,Mn(t))}),ao.functor=En,ao.xhr=An(m),ao.dsv=function(n,t){function e(n,e,u){arguments.length<3&&(u=e,e=null);var o=Cn(n,t,null==e?r:i(e),u);return o.row=function(n){return arguments.length?o.response(null==(e=n)?r:i(n)):e},o}function r(n){return e.parse(n.responseText)}function i(n){return function(t){return e.parse(t.responseText,n)}}function u(t){return t.map(o).join(n)}function o(n){return a.test(n)?'"'+n.replace(/\"/g,'""')+'"':n}var a=new RegExp('["'+n+"\n]"),l=n.charCodeAt(0);return e.parse=function(n,t){var r;return e.parseRows(n,function(n,e){if(r)return r(n,e-1);var i=new Function("d","return {"+n.map(function(n,t){return JSON.stringify(n)+": d["+t+"]"}).join(",")+"}");r=t?function(n,e){return t(i(n),e)}:i})},e.parseRows=function(n,t){function e(){if(f>=c)return o;if(i)return i=!1,u;var t=f;if(34===n.charCodeAt(t)){for(var e=t;e++f;){var r=n.charCodeAt(f++),a=1;if(10===r)i=!0;else if(13===r)i=!0,10===n.charCodeAt(f)&&(++f,++a);else if(r!==l)continue;return n.slice(t,f-a)}return n.slice(t)}for(var r,i,u={},o={},a=[],c=n.length,f=0,s=0;(r=e())!==o;){for(var h=[];r!==u&&r!==o;)h.push(r),r=e();t&&null==(h=t(h,s++))||a.push(h)}return a},e.format=function(t){if(Array.isArray(t[0]))return e.formatRows(t);var r=new y,i=[];return t.forEach(function(n){for(var t in n)r.has(t)||i.push(r.add(t))}),[i.map(o).join(n)].concat(t.map(function(t){return i.map(function(n){return o(t[n])}).join(n)})).join("\n")},e.formatRows=function(n){return n.map(u).join("\n")},e},ao.csv=ao.dsv(",","text/csv"),ao.tsv=ao.dsv(" ","text/tab-separated-values");var oa,aa,la,ca,fa=this[x(this,"requestAnimationFrame")]||function(n){setTimeout(n,17)};ao.timer=function(){qn.apply(this,arguments)},ao.timer.flush=function(){Rn(),Dn()},ao.round=function(n,t){return t?Math.round(n*(t=Math.pow(10,t)))/t:Math.round(n)};var sa=["y","z","a","f","p","n","\xb5","m","","k","M","G","T","P","E","Z","Y"].map(Un);ao.formatPrefix=function(n,t){var e=0;return(n=+n)&&(0>n&&(n*=-1),t&&(n=ao.round(n,Pn(n,t))),e=1+Math.floor(1e-12+Math.log(n)/Math.LN10),e=Math.max(-24,Math.min(24,3*Math.floor((e-1)/3)))),sa[8+e/3]};var ha=/(?:([^{])?([<>=^]))?([+\- ])?([$#])?(0)?(\d+)?(,)?(\.-?\d+)?([a-z%])?/i,pa=ao.map({b:function(n){return n.toString(2)},c:function(n){return String.fromCharCode(n)},o:function(n){return n.toString(8)},x:function(n){return n.toString(16)},X:function(n){return n.toString(16).toUpperCase()},g:function(n,t){return n.toPrecision(t)},e:function(n,t){return n.toExponential(t)},f:function(n,t){return n.toFixed(t)},r:function(n,t){return(n=ao.round(n,Pn(n,t))).toFixed(Math.max(0,Math.min(20,Pn(n*(1+1e-15),t))))}}),ga=ao.time={},va=Date;Hn.prototype={getDate:function(){return this._.getUTCDate()},getDay:function(){return this._.getUTCDay()},getFullYear:function(){return this._.getUTCFullYear()},getHours:function(){return this._.getUTCHours()},getMilliseconds:function(){return this._.getUTCMilliseconds()},getMinutes:function(){return this._.getUTCMinutes()},getMonth:function(){return this._.getUTCMonth()},getSeconds:function(){return this._.getUTCSeconds()},getTime:function(){return this._.getTime()},getTimezoneOffset:function(){return 0},valueOf:function(){return this._.valueOf()},setDate:function(){da.setUTCDate.apply(this._,arguments)},setDay:function(){da.setUTCDay.apply(this._,arguments)},setFullYear:function(){da.setUTCFullYear.apply(this._,arguments)},setHours:function(){da.setUTCHours.apply(this._,arguments)},setMilliseconds:function(){da.setUTCMilliseconds.apply(this._,arguments)},setMinutes:function(){da.setUTCMinutes.apply(this._,arguments)},setMonth:function(){da.setUTCMonth.apply(this._,arguments)},setSeconds:function(){da.setUTCSeconds.apply(this._,arguments)},setTime:function(){da.setTime.apply(this._,arguments)}};var da=Date.prototype;ga.year=On(function(n){return n=ga.day(n),n.setMonth(0,1),n},function(n,t){n.setFullYear(n.getFullYear()+t)},function(n){return n.getFullYear()}),ga.years=ga.year.range,ga.years.utc=ga.year.utc.range,ga.day=On(function(n){var t=new va(2e3,0);return t.setFullYear(n.getFullYear(),n.getMonth(),n.getDate()),t},function(n,t){n.setDate(n.getDate()+t)},function(n){return n.getDate()-1}),ga.days=ga.day.range,ga.days.utc=ga.day.utc.range,ga.dayOfYear=function(n){var t=ga.year(n);return Math.floor((n-t-6e4*(n.getTimezoneOffset()-t.getTimezoneOffset()))/864e5)},["sunday","monday","tuesday","wednesday","thursday","friday","saturday"].forEach(function(n,t){t=7-t;var e=ga[n]=On(function(n){return(n=ga.day(n)).setDate(n.getDate()-(n.getDay()+t)%7),n},function(n,t){n.setDate(n.getDate()+7*Math.floor(t))},function(n){var e=ga.year(n).getDay();return Math.floor((ga.dayOfYear(n)+(e+t)%7)/7)-(e!==t)});ga[n+"s"]=e.range,ga[n+"s"].utc=e.utc.range,ga[n+"OfYear"]=function(n){var e=ga.year(n).getDay();return Math.floor((ga.dayOfYear(n)+(e+t)%7)/7)}}),ga.week=ga.sunday,ga.weeks=ga.sunday.range,ga.weeks.utc=ga.sunday.utc.range,ga.weekOfYear=ga.sundayOfYear;var ya={"-":"",_:" ",0:"0"},ma=/^\s*\d+/,Ma=/^%/;ao.locale=function(n){return{numberFormat:jn(n),timeFormat:Yn(n)}};var xa=ao.locale({decimal:".",thousands:",",grouping:[3],currency:["$",""],dateTime:"%a %b %e %X %Y",date:"%m/%d/%Y",time:"%H:%M:%S",periods:["AM","PM"],days:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"], shortDays:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],months:["January","February","March","April","May","June","July","August","September","October","November","December"],shortMonths:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"]});ao.format=xa.numberFormat,ao.geo={},ft.prototype={s:0,t:0,add:function(n){st(n,this.t,ba),st(ba.s,this.s,this),this.s?this.t+=ba.t:this.s=ba.t},reset:function(){this.s=this.t=0},valueOf:function(){return this.s}};var ba=new ft;ao.geo.stream=function(n,t){n&&_a.hasOwnProperty(n.type)?_a[n.type](n,t):ht(n,t)};var _a={Feature:function(n,t){ht(n.geometry,t)},FeatureCollection:function(n,t){for(var e=n.features,r=-1,i=e.length;++rn?4*Fo+n:n,Na.lineStart=Na.lineEnd=Na.point=b}};ao.geo.bounds=function(){function n(n,t){M.push(x=[f=n,h=n]),s>t&&(s=t),t>p&&(p=t)}function t(t,e){var r=dt([t*Yo,e*Yo]);if(y){var i=mt(y,r),u=[i[1],-i[0],0],o=mt(u,i);bt(o),o=_t(o);var l=t-g,c=l>0?1:-1,v=o[0]*Zo*c,d=xo(l)>180;if(d^(v>c*g&&c*t>v)){var m=o[1]*Zo;m>p&&(p=m)}else if(v=(v+360)%360-180,d^(v>c*g&&c*t>v)){var m=-o[1]*Zo;s>m&&(s=m)}else s>e&&(s=e),e>p&&(p=e);d?g>t?a(f,t)>a(f,h)&&(h=t):a(t,h)>a(f,h)&&(f=t):h>=f?(f>t&&(f=t),t>h&&(h=t)):t>g?a(f,t)>a(f,h)&&(h=t):a(t,h)>a(f,h)&&(f=t)}else n(t,e);y=r,g=t}function e(){b.point=t}function r(){x[0]=f,x[1]=h,b.point=n,y=null}function i(n,e){if(y){var r=n-g;m+=xo(r)>180?r+(r>0?360:-360):r}else v=n,d=e;Na.point(n,e),t(n,e)}function u(){Na.lineStart()}function o(){i(v,d),Na.lineEnd(),xo(m)>Uo&&(f=-(h=180)),x[0]=f,x[1]=h,y=null}function a(n,t){return(t-=n)<0?t+360:t}function l(n,t){return n[0]-t[0]}function c(n,t){return t[0]<=t[1]?t[0]<=n&&n<=t[1]:nka?(f=-(h=180),s=-(p=90)):m>Uo?p=90:-Uo>m&&(s=-90),x[0]=f,x[1]=h}};return function(n){p=h=-(f=s=1/0),M=[],ao.geo.stream(n,b);var t=M.length;if(t){M.sort(l);for(var e,r=1,i=M[0],u=[i];t>r;++r)e=M[r],c(e[0],i)||c(e[1],i)?(a(i[0],e[1])>a(i[0],i[1])&&(i[1]=e[1]),a(e[0],i[1])>a(i[0],i[1])&&(i[0]=e[0])):u.push(i=e);for(var o,e,g=-(1/0),t=u.length-1,r=0,i=u[t];t>=r;i=e,++r)e=u[r],(o=a(i[1],e[0]))>g&&(g=o,f=e[0],h=i[1])}return M=x=null,f===1/0||s===1/0?[[NaN,NaN],[NaN,NaN]]:[[f,s],[h,p]]}}(),ao.geo.centroid=function(n){Ea=Aa=Ca=za=La=qa=Ta=Ra=Da=Pa=Ua=0,ao.geo.stream(n,ja);var t=Da,e=Pa,r=Ua,i=t*t+e*e+r*r;return jo>i&&(t=qa,e=Ta,r=Ra,Uo>Aa&&(t=Ca,e=za,r=La),i=t*t+e*e+r*r,jo>i)?[NaN,NaN]:[Math.atan2(e,t)*Zo,tn(r/Math.sqrt(i))*Zo]};var Ea,Aa,Ca,za,La,qa,Ta,Ra,Da,Pa,Ua,ja={sphere:b,point:St,lineStart:Nt,lineEnd:Et,polygonStart:function(){ja.lineStart=At},polygonEnd:function(){ja.lineStart=Nt}},Fa=Rt(zt,jt,Ht,[-Fo,-Fo/2]),Ha=1e9;ao.geo.clipExtent=function(){var n,t,e,r,i,u,o={stream:function(n){return i&&(i.valid=!1),i=u(n),i.valid=!0,i},extent:function(a){return arguments.length?(u=Zt(n=+a[0][0],t=+a[0][1],e=+a[1][0],r=+a[1][1]),i&&(i.valid=!1,i=null),o):[[n,t],[e,r]]}};return o.extent([[0,0],[960,500]])},(ao.geo.conicEqualArea=function(){return Vt(Xt)}).raw=Xt,ao.geo.albers=function(){return ao.geo.conicEqualArea().rotate([96,0]).center([-.6,38.7]).parallels([29.5,45.5]).scale(1070)},ao.geo.albersUsa=function(){function n(n){var u=n[0],o=n[1];return t=null,e(u,o),t||(r(u,o),t)||i(u,o),t}var t,e,r,i,u=ao.geo.albers(),o=ao.geo.conicEqualArea().rotate([154,0]).center([-2,58.5]).parallels([55,65]),a=ao.geo.conicEqualArea().rotate([157,0]).center([-3,19.9]).parallels([8,18]),l={point:function(n,e){t=[n,e]}};return n.invert=function(n){var t=u.scale(),e=u.translate(),r=(n[0]-e[0])/t,i=(n[1]-e[1])/t;return(i>=.12&&.234>i&&r>=-.425&&-.214>r?o:i>=.166&&.234>i&&r>=-.214&&-.115>r?a:u).invert(n)},n.stream=function(n){var t=u.stream(n),e=o.stream(n),r=a.stream(n);return{point:function(n,i){t.point(n,i),e.point(n,i),r.point(n,i)},sphere:function(){t.sphere(),e.sphere(),r.sphere()},lineStart:function(){t.lineStart(),e.lineStart(),r.lineStart()},lineEnd:function(){t.lineEnd(),e.lineEnd(),r.lineEnd()},polygonStart:function(){t.polygonStart(),e.polygonStart(),r.polygonStart()},polygonEnd:function(){t.polygonEnd(),e.polygonEnd(),r.polygonEnd()}}},n.precision=function(t){return arguments.length?(u.precision(t),o.precision(t),a.precision(t),n):u.precision()},n.scale=function(t){return arguments.length?(u.scale(t),o.scale(.35*t),a.scale(t),n.translate(u.translate())):u.scale()},n.translate=function(t){if(!arguments.length)return u.translate();var c=u.scale(),f=+t[0],s=+t[1];return e=u.translate(t).clipExtent([[f-.455*c,s-.238*c],[f+.455*c,s+.238*c]]).stream(l).point,r=o.translate([f-.307*c,s+.201*c]).clipExtent([[f-.425*c+Uo,s+.12*c+Uo],[f-.214*c-Uo,s+.234*c-Uo]]).stream(l).point,i=a.translate([f-.205*c,s+.212*c]).clipExtent([[f-.214*c+Uo,s+.166*c+Uo],[f-.115*c-Uo,s+.234*c-Uo]]).stream(l).point,n},n.scale(1070)};var Oa,Ia,Ya,Za,Va,Xa,$a={point:b,lineStart:b,lineEnd:b,polygonStart:function(){Ia=0,$a.lineStart=$t},polygonEnd:function(){$a.lineStart=$a.lineEnd=$a.point=b,Oa+=xo(Ia/2)}},Ba={point:Bt,lineStart:b,lineEnd:b,polygonStart:b,polygonEnd:b},Wa={point:Gt,lineStart:Kt,lineEnd:Qt,polygonStart:function(){Wa.lineStart=ne},polygonEnd:function(){Wa.point=Gt,Wa.lineStart=Kt,Wa.lineEnd=Qt}};ao.geo.path=function(){function n(n){return n&&("function"==typeof a&&u.pointRadius(+a.apply(this,arguments)),o&&o.valid||(o=i(u)),ao.geo.stream(n,o)),u.result()}function t(){return o=null,n}var e,r,i,u,o,a=4.5;return n.area=function(n){return Oa=0,ao.geo.stream(n,i($a)),Oa},n.centroid=function(n){return Ca=za=La=qa=Ta=Ra=Da=Pa=Ua=0,ao.geo.stream(n,i(Wa)),Ua?[Da/Ua,Pa/Ua]:Ra?[qa/Ra,Ta/Ra]:La?[Ca/La,za/La]:[NaN,NaN]},n.bounds=function(n){return Va=Xa=-(Ya=Za=1/0),ao.geo.stream(n,i(Ba)),[[Ya,Za],[Va,Xa]]},n.projection=function(n){return arguments.length?(i=(e=n)?n.stream||re(n):m,t()):e},n.context=function(n){return arguments.length?(u=null==(r=n)?new Wt:new te(n),"function"!=typeof a&&u.pointRadius(a),t()):r},n.pointRadius=function(t){return arguments.length?(a="function"==typeof t?t:(u.pointRadius(+t),+t),n):a},n.projection(ao.geo.albersUsa()).context(null)},ao.geo.transform=function(n){return{stream:function(t){var e=new ie(t);for(var r in n)e[r]=n[r];return e}}},ie.prototype={point:function(n,t){this.stream.point(n,t)},sphere:function(){this.stream.sphere()},lineStart:function(){this.stream.lineStart()},lineEnd:function(){this.stream.lineEnd()},polygonStart:function(){this.stream.polygonStart()},polygonEnd:function(){this.stream.polygonEnd()}},ao.geo.projection=oe,ao.geo.projectionMutator=ae,(ao.geo.equirectangular=function(){return oe(ce)}).raw=ce.invert=ce,ao.geo.rotation=function(n){function t(t){return t=n(t[0]*Yo,t[1]*Yo),t[0]*=Zo,t[1]*=Zo,t}return n=se(n[0]%360*Yo,n[1]*Yo,n.length>2?n[2]*Yo:0),t.invert=function(t){return t=n.invert(t[0]*Yo,t[1]*Yo),t[0]*=Zo,t[1]*=Zo,t},t},fe.invert=ce,ao.geo.circle=function(){function n(){var n="function"==typeof r?r.apply(this,arguments):r,t=se(-n[0]*Yo,-n[1]*Yo,0).invert,i=[];return e(null,null,1,{point:function(n,e){i.push(n=t(n,e)),n[0]*=Zo,n[1]*=Zo}}),{type:"Polygon",coordinates:[i]}}var t,e,r=[0,0],i=6;return n.origin=function(t){return arguments.length?(r=t,n):r},n.angle=function(r){return arguments.length?(e=ve((t=+r)*Yo,i*Yo),n):t},n.precision=function(r){return arguments.length?(e=ve(t*Yo,(i=+r)*Yo),n):i},n.angle(90)},ao.geo.distance=function(n,t){var e,r=(t[0]-n[0])*Yo,i=n[1]*Yo,u=t[1]*Yo,o=Math.sin(r),a=Math.cos(r),l=Math.sin(i),c=Math.cos(i),f=Math.sin(u),s=Math.cos(u);return Math.atan2(Math.sqrt((e=s*o)*e+(e=c*f-l*s*a)*e),l*f+c*s*a)},ao.geo.graticule=function(){function n(){return{type:"MultiLineString",coordinates:t()}}function t(){return ao.range(Math.ceil(u/d)*d,i,d).map(h).concat(ao.range(Math.ceil(c/y)*y,l,y).map(p)).concat(ao.range(Math.ceil(r/g)*g,e,g).filter(function(n){return xo(n%d)>Uo}).map(f)).concat(ao.range(Math.ceil(a/v)*v,o,v).filter(function(n){return xo(n%y)>Uo}).map(s))}var e,r,i,u,o,a,l,c,f,s,h,p,g=10,v=g,d=90,y=360,m=2.5;return n.lines=function(){return t().map(function(n){return{type:"LineString",coordinates:n}})},n.outline=function(){return{type:"Polygon",coordinates:[h(u).concat(p(l).slice(1),h(i).reverse().slice(1),p(c).reverse().slice(1))]}},n.extent=function(t){return arguments.length?n.majorExtent(t).minorExtent(t):n.minorExtent()},n.majorExtent=function(t){return arguments.length?(u=+t[0][0],i=+t[1][0],c=+t[0][1],l=+t[1][1],u>i&&(t=u,u=i,i=t),c>l&&(t=c,c=l,l=t),n.precision(m)):[[u,c],[i,l]]},n.minorExtent=function(t){return arguments.length?(r=+t[0][0],e=+t[1][0],a=+t[0][1],o=+t[1][1],r>e&&(t=r,r=e,e=t),a>o&&(t=a,a=o,o=t),n.precision(m)):[[r,a],[e,o]]},n.step=function(t){return arguments.length?n.majorStep(t).minorStep(t):n.minorStep()},n.majorStep=function(t){return arguments.length?(d=+t[0],y=+t[1],n):[d,y]},n.minorStep=function(t){return arguments.length?(g=+t[0],v=+t[1],n):[g,v]},n.precision=function(t){return arguments.length?(m=+t,f=ye(a,o,90),s=me(r,e,m),h=ye(c,l,90),p=me(u,i,m),n):m},n.majorExtent([[-180,-90+Uo],[180,90-Uo]]).minorExtent([[-180,-80-Uo],[180,80+Uo]])},ao.geo.greatArc=function(){function n(){return{type:"LineString",coordinates:[t||r.apply(this,arguments),e||i.apply(this,arguments)]}}var t,e,r=Me,i=xe;return n.distance=function(){return ao.geo.distance(t||r.apply(this,arguments),e||i.apply(this,arguments))},n.source=function(e){return arguments.length?(r=e,t="function"==typeof e?null:e,n):r},n.target=function(t){return arguments.length?(i=t,e="function"==typeof t?null:t,n):i},n.precision=function(){return arguments.length?n:0},n},ao.geo.interpolate=function(n,t){return be(n[0]*Yo,n[1]*Yo,t[0]*Yo,t[1]*Yo)},ao.geo.length=function(n){return Ja=0,ao.geo.stream(n,Ga),Ja};var Ja,Ga={sphere:b,point:b,lineStart:_e,lineEnd:b,polygonStart:b,polygonEnd:b},Ka=we(function(n){return Math.sqrt(2/(1+n))},function(n){return 2*Math.asin(n/2)});(ao.geo.azimuthalEqualArea=function(){return oe(Ka)}).raw=Ka;var Qa=we(function(n){var t=Math.acos(n);return t&&t/Math.sin(t)},m);(ao.geo.azimuthalEquidistant=function(){return oe(Qa)}).raw=Qa,(ao.geo.conicConformal=function(){return Vt(Se)}).raw=Se,(ao.geo.conicEquidistant=function(){return Vt(ke)}).raw=ke;var nl=we(function(n){return 1/n},Math.atan);(ao.geo.gnomonic=function(){return oe(nl)}).raw=nl,Ne.invert=function(n,t){return[n,2*Math.atan(Math.exp(t))-Io]},(ao.geo.mercator=function(){return Ee(Ne)}).raw=Ne;var tl=we(function(){return 1},Math.asin);(ao.geo.orthographic=function(){return oe(tl)}).raw=tl;var el=we(function(n){return 1/(1+n)},function(n){return 2*Math.atan(n)});(ao.geo.stereographic=function(){return oe(el)}).raw=el,Ae.invert=function(n,t){return[-t,2*Math.atan(Math.exp(n))-Io]},(ao.geo.transverseMercator=function(){var n=Ee(Ae),t=n.center,e=n.rotate;return n.center=function(n){return n?t([-n[1],n[0]]):(n=t(),[n[1],-n[0]])},n.rotate=function(n){return n?e([n[0],n[1],n.length>2?n[2]+90:90]):(n=e(),[n[0],n[1],n[2]-90])},e([0,0,90])}).raw=Ae,ao.geom={},ao.geom.hull=function(n){function t(n){if(n.length<3)return[];var t,i=En(e),u=En(r),o=n.length,a=[],l=[];for(t=0;o>t;t++)a.push([+i.call(this,n[t],t),+u.call(this,n[t],t),t]);for(a.sort(qe),t=0;o>t;t++)l.push([a[t][0],-a[t][1]]);var c=Le(a),f=Le(l),s=f[0]===c[0],h=f[f.length-1]===c[c.length-1],p=[];for(t=c.length-1;t>=0;--t)p.push(n[a[c[t]][2]]);for(t=+s;t=r&&c.x<=u&&c.y>=i&&c.y<=o?[[r,o],[u,o],[u,i],[r,i]]:[];f.point=n[a]}),t}function e(n){return n.map(function(n,t){return{x:Math.round(u(n,t)/Uo)*Uo,y:Math.round(o(n,t)/Uo)*Uo,i:t}})}var r=Ce,i=ze,u=r,o=i,a=sl;return n?t(n):(t.links=function(n){return ar(e(n)).edges.filter(function(n){return n.l&&n.r}).map(function(t){return{source:n[t.l.i],target:n[t.r.i]}})},t.triangles=function(n){var t=[];return ar(e(n)).cells.forEach(function(e,r){for(var i,u,o=e.site,a=e.edges.sort(Ve),l=-1,c=a.length,f=a[c-1].edge,s=f.l===o?f.r:f.l;++l=c,h=r>=f,p=h<<1|s;n.leaf=!1,n=n.nodes[p]||(n.nodes[p]=hr()),s?i=c:a=c,h?o=f:l=f,u(n,t,e,r,i,o,a,l)}var f,s,h,p,g,v,d,y,m,M=En(a),x=En(l);if(null!=t)v=t,d=e,y=r,m=i;else if(y=m=-(v=d=1/0),s=[],h=[],g=n.length,o)for(p=0;g>p;++p)f=n[p],f.xy&&(y=f.x),f.y>m&&(m=f.y),s.push(f.x),h.push(f.y);else for(p=0;g>p;++p){var b=+M(f=n[p],p),_=+x(f,p);v>b&&(v=b),d>_&&(d=_),b>y&&(y=b),_>m&&(m=_),s.push(b),h.push(_)}var w=y-v,S=m-d;w>S?m=d+w:y=v+S;var k=hr();if(k.add=function(n){u(k,n,+M(n,++p),+x(n,p),v,d,y,m)},k.visit=function(n){pr(n,k,v,d,y,m)},k.find=function(n){return gr(k,n[0],n[1],v,d,y,m)},p=-1,null==t){for(;++p=0?n.slice(0,t):n,r=t>=0?n.slice(t+1):"in";return e=vl.get(e)||gl,r=dl.get(r)||m,br(r(e.apply(null,lo.call(arguments,1))))},ao.interpolateHcl=Rr,ao.interpolateHsl=Dr,ao.interpolateLab=Pr,ao.interpolateRound=Ur,ao.transform=function(n){var t=fo.createElementNS(ao.ns.prefix.svg,"g");return(ao.transform=function(n){if(null!=n){t.setAttribute("transform",n);var e=t.transform.baseVal.consolidate()}return new jr(e?e.matrix:yl)})(n)},jr.prototype.toString=function(){return"translate("+this.translate+")rotate("+this.rotate+")skewX("+this.skew+")scale("+this.scale+")"};var yl={a:1,b:0,c:0,d:1,e:0,f:0};ao.interpolateTransform=$r,ao.layout={},ao.layout.bundle=function(){return function(n){for(var t=[],e=-1,r=n.length;++ea*a/y){if(v>l){var c=t.charge/l;n.px-=u*c,n.py-=o*c}return!0}if(t.point&&l&&v>l){var c=t.pointCharge/l;n.px-=u*c,n.py-=o*c}}return!t.charge}}function t(n){n.px=ao.event.x,n.py=ao.event.y,l.resume()}var e,r,i,u,o,a,l={},c=ao.dispatch("start","tick","end"),f=[1,1],s=.9,h=ml,p=Ml,g=-30,v=xl,d=.1,y=.64,M=[],x=[];return l.tick=function(){if((i*=.99)<.005)return e=null,c.end({type:"end",alpha:i=0}),!0;var t,r,l,h,p,v,y,m,b,_=M.length,w=x.length;for(r=0;w>r;++r)l=x[r],h=l.source,p=l.target,m=p.x-h.x,b=p.y-h.y,(v=m*m+b*b)&&(v=i*o[r]*((v=Math.sqrt(v))-u[r])/v,m*=v,b*=v,p.x-=m*(y=h.weight+p.weight?h.weight/(h.weight+p.weight):.5),p.y-=b*y,h.x+=m*(y=1-y),h.y+=b*y);if((y=i*d)&&(m=f[0]/2,b=f[1]/2,r=-1,y))for(;++r<_;)l=M[r],l.x+=(m-l.x)*y,l.y+=(b-l.y)*y;if(g)for(ri(t=ao.geom.quadtree(M),i,a),r=-1;++r<_;)(l=M[r]).fixed||t.visit(n(l));for(r=-1;++r<_;)l=M[r],l.fixed?(l.x=l.px,l.y=l.py):(l.x-=(l.px-(l.px=l.x))*s,l.y-=(l.py-(l.py=l.y))*s);c.tick({type:"tick",alpha:i})},l.nodes=function(n){return arguments.length?(M=n,l):M},l.links=function(n){return arguments.length?(x=n,l):x},l.size=function(n){return arguments.length?(f=n,l):f},l.linkDistance=function(n){return arguments.length?(h="function"==typeof n?n:+n,l):h},l.distance=l.linkDistance,l.linkStrength=function(n){return arguments.length?(p="function"==typeof n?n:+n,l):p},l.friction=function(n){return arguments.length?(s=+n,l):s},l.charge=function(n){return arguments.length?(g="function"==typeof n?n:+n,l):g},l.chargeDistance=function(n){return arguments.length?(v=n*n,l):Math.sqrt(v)},l.gravity=function(n){return arguments.length?(d=+n,l):d},l.theta=function(n){return arguments.length?(y=n*n,l):Math.sqrt(y)},l.alpha=function(n){return arguments.length?(n=+n,i?n>0?i=n:(e.c=null,e.t=NaN,e=null,c.end({type:"end",alpha:i=0})):n>0&&(c.start({type:"start",alpha:i=n}),e=qn(l.tick)),l):i},l.start=function(){function n(n,r){if(!e){for(e=new Array(i),l=0;i>l;++l)e[l]=[];for(l=0;c>l;++l){var u=x[l];e[u.source.index].push(u.target),e[u.target.index].push(u.source)}}for(var o,a=e[t],l=-1,f=a.length;++lt;++t)(r=M[t]).index=t,r.weight=0;for(t=0;c>t;++t)r=x[t],"number"==typeof r.source&&(r.source=M[r.source]),"number"==typeof r.target&&(r.target=M[r.target]),++r.source.weight,++r.target.weight;for(t=0;i>t;++t)r=M[t],isNaN(r.x)&&(r.x=n("x",s)),isNaN(r.y)&&(r.y=n("y",v)),isNaN(r.px)&&(r.px=r.x),isNaN(r.py)&&(r.py=r.y);if(u=[],"function"==typeof h)for(t=0;c>t;++t)u[t]=+h.call(this,x[t],t);else for(t=0;c>t;++t)u[t]=h;if(o=[],"function"==typeof p)for(t=0;c>t;++t)o[t]=+p.call(this,x[t],t);else for(t=0;c>t;++t)o[t]=p;if(a=[],"function"==typeof g)for(t=0;i>t;++t)a[t]=+g.call(this,M[t],t);else for(t=0;i>t;++t)a[t]=g;return l.resume()},l.resume=function(){return l.alpha(.1)},l.stop=function(){return l.alpha(0)},l.drag=function(){return r||(r=ao.behavior.drag().origin(m).on("dragstart.force",Qr).on("drag.force",t).on("dragend.force",ni)),arguments.length?void this.on("mouseover.force",ti).on("mouseout.force",ei).call(r):r},ao.rebind(l,c,"on")};var ml=20,Ml=1,xl=1/0;ao.layout.hierarchy=function(){function n(i){var u,o=[i],a=[];for(i.depth=0;null!=(u=o.pop());)if(a.push(u),(c=e.call(n,u,u.depth))&&(l=c.length)){for(var l,c,f;--l>=0;)o.push(f=c[l]),f.parent=u,f.depth=u.depth+1;r&&(u.value=0),u.children=c}else r&&(u.value=+r.call(n,u,u.depth)||0),delete u.children;return oi(i,function(n){var e,i;t&&(e=n.children)&&e.sort(t),r&&(i=n.parent)&&(i.value+=n.value)}),a}var t=ci,e=ai,r=li;return n.sort=function(e){return arguments.length?(t=e,n):t},n.children=function(t){return arguments.length?(e=t,n):e},n.value=function(t){return arguments.length?(r=t,n):r},n.revalue=function(t){return r&&(ui(t,function(n){n.children&&(n.value=0)}),oi(t,function(t){var e;t.children||(t.value=+r.call(n,t,t.depth)||0),(e=t.parent)&&(e.value+=t.value)})),t},n},ao.layout.partition=function(){function n(t,e,r,i){var u=t.children;if(t.x=e,t.y=t.depth*i,t.dx=r,t.dy=i,u&&(o=u.length)){var o,a,l,c=-1;for(r=t.value?r/t.value:0;++cs?-1:1),g=ao.sum(c),v=g?(s-l*p)/g:0,d=ao.range(l),y=[];return null!=e&&d.sort(e===bl?function(n,t){return c[t]-c[n]}:function(n,t){return e(o[n],o[t])}),d.forEach(function(n){y[n]={data:o[n],value:a=c[n],startAngle:f,endAngle:f+=a*v+p,padAngle:h}}),y}var t=Number,e=bl,r=0,i=Ho,u=0;return n.value=function(e){return arguments.length?(t=e,n):t},n.sort=function(t){return arguments.length?(e=t,n):e},n.startAngle=function(t){return arguments.length?(r=t,n):r},n.endAngle=function(t){return arguments.length?(i=t,n):i},n.padAngle=function(t){return arguments.length?(u=t,n):u},n};var bl={};ao.layout.stack=function(){function n(a,l){if(!(h=a.length))return a;var c=a.map(function(e,r){return t.call(n,e,r)}),f=c.map(function(t){return t.map(function(t,e){return[u.call(n,t,e),o.call(n,t,e)]})}),s=e.call(n,f,l);c=ao.permute(c,s),f=ao.permute(f,s);var h,p,g,v,d=r.call(n,f,l),y=c[0].length;for(g=0;y>g;++g)for(i.call(n,c[0][g],v=d[g],f[0][g][1]),p=1;h>p;++p)i.call(n,c[p][g],v+=f[p-1][g][1],f[p][g][1]);return a}var t=m,e=gi,r=vi,i=pi,u=si,o=hi;return n.values=function(e){return arguments.length?(t=e,n):t},n.order=function(t){return arguments.length?(e="function"==typeof t?t:_l.get(t)||gi,n):e},n.offset=function(t){return arguments.length?(r="function"==typeof t?t:wl.get(t)||vi,n):r},n.x=function(t){return arguments.length?(u=t,n):u},n.y=function(t){return arguments.length?(o=t,n):o},n.out=function(t){return arguments.length?(i=t,n):i},n};var _l=ao.map({"inside-out":function(n){var t,e,r=n.length,i=n.map(di),u=n.map(yi),o=ao.range(r).sort(function(n,t){return i[n]-i[t]}),a=0,l=0,c=[],f=[];for(t=0;r>t;++t)e=o[t],l>a?(a+=u[e],c.push(e)):(l+=u[e],f.push(e));return f.reverse().concat(c)},reverse:function(n){return ao.range(n.length).reverse()},"default":gi}),wl=ao.map({silhouette:function(n){var t,e,r,i=n.length,u=n[0].length,o=[],a=0,l=[];for(e=0;u>e;++e){for(t=0,r=0;i>t;t++)r+=n[t][e][1];r>a&&(a=r),o.push(r)}for(e=0;u>e;++e)l[e]=(a-o[e])/2;return l},wiggle:function(n){var t,e,r,i,u,o,a,l,c,f=n.length,s=n[0],h=s.length,p=[];for(p[0]=l=c=0,e=1;h>e;++e){for(t=0,i=0;f>t;++t)i+=n[t][e][1];for(t=0,u=0,a=s[e][0]-s[e-1][0];f>t;++t){for(r=0,o=(n[t][e][1]-n[t][e-1][1])/(2*a);t>r;++r)o+=(n[r][e][1]-n[r][e-1][1])/a;u+=o*n[t][e][1]}p[e]=l-=i?u/i*a:0,c>l&&(c=l)}for(e=0;h>e;++e)p[e]-=c;return p},expand:function(n){var t,e,r,i=n.length,u=n[0].length,o=1/i,a=[];for(e=0;u>e;++e){for(t=0,r=0;i>t;t++)r+=n[t][e][1];if(r)for(t=0;i>t;t++)n[t][e][1]/=r;else for(t=0;i>t;t++)n[t][e][1]=o}for(e=0;u>e;++e)a[e]=0;return a},zero:vi});ao.layout.histogram=function(){function n(n,u){for(var o,a,l=[],c=n.map(e,this),f=r.call(this,c,u),s=i.call(this,f,c,u),u=-1,h=c.length,p=s.length-1,g=t?1:1/h;++u0)for(u=-1;++u=f[0]&&a<=f[1]&&(o=l[ao.bisect(s,a,1,p)-1],o.y+=g,o.push(n[u]));return l}var t=!0,e=Number,r=bi,i=Mi;return n.value=function(t){return arguments.length?(e=t,n):e},n.range=function(t){return arguments.length?(r=En(t),n):r},n.bins=function(t){return arguments.length?(i="number"==typeof t?function(n){return xi(n,t)}:En(t),n):i},n.frequency=function(e){return arguments.length?(t=!!e,n):t},n},ao.layout.pack=function(){function n(n,u){var o=e.call(this,n,u),a=o[0],l=i[0],c=i[1],f=null==t?Math.sqrt:"function"==typeof t?t:function(){return t};if(a.x=a.y=0,oi(a,function(n){n.r=+f(n.value)}),oi(a,Ni),r){var s=r*(t?1:Math.max(2*a.r/l,2*a.r/c))/2;oi(a,function(n){n.r+=s}),oi(a,Ni),oi(a,function(n){n.r-=s})}return Ci(a,l/2,c/2,t?1:1/Math.max(2*a.r/l,2*a.r/c)),o}var t,e=ao.layout.hierarchy().sort(_i),r=0,i=[1,1];return n.size=function(t){return arguments.length?(i=t,n):i},n.radius=function(e){return arguments.length?(t=null==e||"function"==typeof e?e:+e,n):t},n.padding=function(t){return arguments.length?(r=+t,n):r},ii(n,e)},ao.layout.tree=function(){function n(n,i){var f=o.call(this,n,i),s=f[0],h=t(s);if(oi(h,e),h.parent.m=-h.z,ui(h,r),c)ui(s,u);else{var p=s,g=s,v=s;ui(s,function(n){n.xg.x&&(g=n),n.depth>v.depth&&(v=n)});var d=a(p,g)/2-p.x,y=l[0]/(g.x+a(g,p)/2+d),m=l[1]/(v.depth||1);ui(s,function(n){n.x=(n.x+d)*y,n.y=n.depth*m})}return f}function t(n){for(var t,e={A:null,children:[n]},r=[e];null!=(t=r.pop());)for(var i,u=t.children,o=0,a=u.length;a>o;++o)r.push((u[o]=i={_:u[o],parent:t,children:(i=u[o].children)&&i.slice()||[],A:null,a:null,z:0,m:0,c:0,s:0,t:null,i:o}).a=i);return e.children[0]}function e(n){var t=n.children,e=n.parent.children,r=n.i?e[n.i-1]:null;if(t.length){Di(n);var u=(t[0].z+t[t.length-1].z)/2;r?(n.z=r.z+a(n._,r._),n.m=n.z-u):n.z=u}else r&&(n.z=r.z+a(n._,r._));n.parent.A=i(n,r,n.parent.A||e[0])}function r(n){n._.x=n.z+n.parent.m,n.m+=n.parent.m}function i(n,t,e){if(t){for(var r,i=n,u=n,o=t,l=i.parent.children[0],c=i.m,f=u.m,s=o.m,h=l.m;o=Ti(o),i=qi(i),o&&i;)l=qi(l),u=Ti(u),u.a=n,r=o.z+s-i.z-c+a(o._,i._),r>0&&(Ri(Pi(o,n,e),n,r),c+=r,f+=r),s+=o.m,c+=i.m,h+=l.m,f+=u.m;o&&!Ti(u)&&(u.t=o,u.m+=s-f),i&&!qi(l)&&(l.t=i,l.m+=c-h,e=n)}return e}function u(n){n.x*=l[0],n.y=n.depth*l[1]}var o=ao.layout.hierarchy().sort(null).value(null),a=Li,l=[1,1],c=null;return n.separation=function(t){return arguments.length?(a=t,n):a},n.size=function(t){return arguments.length?(c=null==(l=t)?u:null,n):c?null:l},n.nodeSize=function(t){return arguments.length?(c=null==(l=t)?null:u,n):c?l:null},ii(n,o)},ao.layout.cluster=function(){function n(n,u){var o,a=t.call(this,n,u),l=a[0],c=0;oi(l,function(n){var t=n.children;t&&t.length?(n.x=ji(t),n.y=Ui(t)):(n.x=o?c+=e(n,o):0,n.y=0,o=n)});var f=Fi(l),s=Hi(l),h=f.x-e(f,s)/2,p=s.x+e(s,f)/2;return oi(l,i?function(n){n.x=(n.x-l.x)*r[0],n.y=(l.y-n.y)*r[1]}:function(n){n.x=(n.x-h)/(p-h)*r[0],n.y=(1-(l.y?n.y/l.y:1))*r[1]}),a}var t=ao.layout.hierarchy().sort(null).value(null),e=Li,r=[1,1],i=!1;return n.separation=function(t){return arguments.length?(e=t,n):e},n.size=function(t){return arguments.length?(i=null==(r=t),n):i?null:r},n.nodeSize=function(t){return arguments.length?(i=null!=(r=t),n):i?r:null},ii(n,t)},ao.layout.treemap=function(){function n(n,t){for(var e,r,i=-1,u=n.length;++it?0:t),e.area=isNaN(r)||0>=r?0:r}function t(e){var u=e.children;if(u&&u.length){var o,a,l,c=s(e),f=[],h=u.slice(),g=1/0,v="slice"===p?c.dx:"dice"===p?c.dy:"slice-dice"===p?1&e.depth?c.dy:c.dx:Math.min(c.dx,c.dy);for(n(h,c.dx*c.dy/e.value),f.area=0;(l=h.length)>0;)f.push(o=h[l-1]),f.area+=o.area,"squarify"!==p||(a=r(f,v))<=g?(h.pop(),g=a):(f.area-=f.pop().area,i(f,v,c,!1),v=Math.min(c.dx,c.dy),f.length=f.area=0,g=1/0);f.length&&(i(f,v,c,!0),f.length=f.area=0),u.forEach(t)}}function e(t){var r=t.children;if(r&&r.length){var u,o=s(t),a=r.slice(),l=[];for(n(a,o.dx*o.dy/t.value),l.area=0;u=a.pop();)l.push(u),l.area+=u.area,null!=u.z&&(i(l,u.z?o.dx:o.dy,o,!a.length),l.length=l.area=0);r.forEach(e)}}function r(n,t){for(var e,r=n.area,i=0,u=1/0,o=-1,a=n.length;++oe&&(u=e),e>i&&(i=e));return r*=r,t*=t,r?Math.max(t*i*g/r,r/(t*u*g)):1/0}function i(n,t,e,r){var i,u=-1,o=n.length,a=e.x,c=e.y,f=t?l(n.area/t):0; if(t==e.dx){for((r||f>e.dy)&&(f=e.dy);++ue.dx)&&(f=e.dx);++ue&&(t=1),1>e&&(n=0),function(){var e,r,i;do e=2*Math.random()-1,r=2*Math.random()-1,i=e*e+r*r;while(!i||i>1);return n+t*e*Math.sqrt(-2*Math.log(i)/i)}},logNormal:function(){var n=ao.random.normal.apply(ao,arguments);return function(){return Math.exp(n())}},bates:function(n){var t=ao.random.irwinHall(n);return function(){return t()/n}},irwinHall:function(n){return function(){for(var t=0,e=0;n>e;e++)t+=Math.random();return t}}},ao.scale={};var Sl={floor:m,ceil:m};ao.scale.linear=function(){return Wi([0,1],[0,1],Mr,!1)};var kl={s:1,g:1,p:1,r:1,e:1};ao.scale.log=function(){return ru(ao.scale.linear().domain([0,1]),10,!0,[1,10])};var Nl=ao.format(".0e"),El={floor:function(n){return-Math.ceil(-n)},ceil:function(n){return-Math.floor(-n)}};ao.scale.pow=function(){return iu(ao.scale.linear(),1,[0,1])},ao.scale.sqrt=function(){return ao.scale.pow().exponent(.5)},ao.scale.ordinal=function(){return ou([],{t:"range",a:[[]]})},ao.scale.category10=function(){return ao.scale.ordinal().range(Al)},ao.scale.category20=function(){return ao.scale.ordinal().range(Cl)},ao.scale.category20b=function(){return ao.scale.ordinal().range(zl)},ao.scale.category20c=function(){return ao.scale.ordinal().range(Ll)};var Al=[2062260,16744206,2924588,14034728,9725885,9197131,14907330,8355711,12369186,1556175].map(xn),Cl=[2062260,11454440,16744206,16759672,2924588,10018698,14034728,16750742,9725885,12955861,9197131,12885140,14907330,16234194,8355711,13092807,12369186,14408589,1556175,10410725].map(xn),zl=[3750777,5395619,7040719,10264286,6519097,9216594,11915115,13556636,9202993,12426809,15186514,15190932,8666169,11356490,14049643,15177372,8077683,10834324,13528509,14589654].map(xn),Ll=[3244733,7057110,10406625,13032431,15095053,16616764,16625259,16634018,3253076,7652470,10607003,13101504,7695281,10394312,12369372,14342891,6513507,9868950,12434877,14277081].map(xn);ao.scale.quantile=function(){return au([],[])},ao.scale.quantize=function(){return lu(0,1,[0,1])},ao.scale.threshold=function(){return cu([.5],[0,1])},ao.scale.identity=function(){return fu([0,1])},ao.svg={},ao.svg.arc=function(){function n(){var n=Math.max(0,+e.apply(this,arguments)),c=Math.max(0,+r.apply(this,arguments)),f=o.apply(this,arguments)-Io,s=a.apply(this,arguments)-Io,h=Math.abs(s-f),p=f>s?0:1;if(n>c&&(g=c,c=n,n=g),h>=Oo)return t(c,p)+(n?t(n,1-p):"")+"Z";var g,v,d,y,m,M,x,b,_,w,S,k,N=0,E=0,A=[];if((y=(+l.apply(this,arguments)||0)/2)&&(d=u===ql?Math.sqrt(n*n+c*c):+u.apply(this,arguments),p||(E*=-1),c&&(E=tn(d/c*Math.sin(y))),n&&(N=tn(d/n*Math.sin(y)))),c){m=c*Math.cos(f+E),M=c*Math.sin(f+E),x=c*Math.cos(s-E),b=c*Math.sin(s-E);var C=Math.abs(s-f-2*E)<=Fo?0:1;if(E&&yu(m,M,x,b)===p^C){var z=(f+s)/2;m=c*Math.cos(z),M=c*Math.sin(z),x=b=null}}else m=M=0;if(n){_=n*Math.cos(s-N),w=n*Math.sin(s-N),S=n*Math.cos(f+N),k=n*Math.sin(f+N);var L=Math.abs(f-s+2*N)<=Fo?0:1;if(N&&yu(_,w,S,k)===1-p^L){var q=(f+s)/2;_=n*Math.cos(q),w=n*Math.sin(q),S=k=null}}else _=w=0;if(h>Uo&&(g=Math.min(Math.abs(c-n)/2,+i.apply(this,arguments)))>.001){v=c>n^p?0:1;var T=g,R=g;if(Fo>h){var D=null==S?[_,w]:null==x?[m,M]:Re([m,M],[S,k],[x,b],[_,w]),P=m-D[0],U=M-D[1],j=x-D[0],F=b-D[1],H=1/Math.sin(Math.acos((P*j+U*F)/(Math.sqrt(P*P+U*U)*Math.sqrt(j*j+F*F)))/2),O=Math.sqrt(D[0]*D[0]+D[1]*D[1]);R=Math.min(g,(n-O)/(H-1)),T=Math.min(g,(c-O)/(H+1))}if(null!=x){var I=mu(null==S?[_,w]:[S,k],[m,M],c,T,p),Y=mu([x,b],[_,w],c,T,p);g===T?A.push("M",I[0],"A",T,",",T," 0 0,",v," ",I[1],"A",c,",",c," 0 ",1-p^yu(I[1][0],I[1][1],Y[1][0],Y[1][1]),",",p," ",Y[1],"A",T,",",T," 0 0,",v," ",Y[0]):A.push("M",I[0],"A",T,",",T," 0 1,",v," ",Y[0])}else A.push("M",m,",",M);if(null!=S){var Z=mu([m,M],[S,k],n,-R,p),V=mu([_,w],null==x?[m,M]:[x,b],n,-R,p);g===R?A.push("L",V[0],"A",R,",",R," 0 0,",v," ",V[1],"A",n,",",n," 0 ",p^yu(V[1][0],V[1][1],Z[1][0],Z[1][1]),",",1-p," ",Z[1],"A",R,",",R," 0 0,",v," ",Z[0]):A.push("L",V[0],"A",R,",",R," 0 0,",v," ",Z[0])}else A.push("L",_,",",w)}else A.push("M",m,",",M),null!=x&&A.push("A",c,",",c," 0 ",C,",",p," ",x,",",b),A.push("L",_,",",w),null!=S&&A.push("A",n,",",n," 0 ",L,",",1-p," ",S,",",k);return A.push("Z"),A.join("")}function t(n,t){return"M0,"+n+"A"+n+","+n+" 0 1,"+t+" 0,"+-n+"A"+n+","+n+" 0 1,"+t+" 0,"+n}var e=hu,r=pu,i=su,u=ql,o=gu,a=vu,l=du;return n.innerRadius=function(t){return arguments.length?(e=En(t),n):e},n.outerRadius=function(t){return arguments.length?(r=En(t),n):r},n.cornerRadius=function(t){return arguments.length?(i=En(t),n):i},n.padRadius=function(t){return arguments.length?(u=t==ql?ql:En(t),n):u},n.startAngle=function(t){return arguments.length?(o=En(t),n):o},n.endAngle=function(t){return arguments.length?(a=En(t),n):a},n.padAngle=function(t){return arguments.length?(l=En(t),n):l},n.centroid=function(){var n=(+e.apply(this,arguments)+ +r.apply(this,arguments))/2,t=(+o.apply(this,arguments)+ +a.apply(this,arguments))/2-Io;return[Math.cos(t)*n,Math.sin(t)*n]},n};var ql="auto";ao.svg.line=function(){return Mu(m)};var Tl=ao.map({linear:xu,"linear-closed":bu,step:_u,"step-before":wu,"step-after":Su,basis:zu,"basis-open":Lu,"basis-closed":qu,bundle:Tu,cardinal:Eu,"cardinal-open":ku,"cardinal-closed":Nu,monotone:Fu});Tl.forEach(function(n,t){t.key=n,t.closed=/-closed$/.test(n)});var Rl=[0,2/3,1/3,0],Dl=[0,1/3,2/3,0],Pl=[0,1/6,2/3,1/6];ao.svg.line.radial=function(){var n=Mu(Hu);return n.radius=n.x,delete n.x,n.angle=n.y,delete n.y,n},wu.reverse=Su,Su.reverse=wu,ao.svg.area=function(){return Ou(m)},ao.svg.area.radial=function(){var n=Ou(Hu);return n.radius=n.x,delete n.x,n.innerRadius=n.x0,delete n.x0,n.outerRadius=n.x1,delete n.x1,n.angle=n.y,delete n.y,n.startAngle=n.y0,delete n.y0,n.endAngle=n.y1,delete n.y1,n},ao.svg.chord=function(){function n(n,a){var l=t(this,u,n,a),c=t(this,o,n,a);return"M"+l.p0+r(l.r,l.p1,l.a1-l.a0)+(e(l,c)?i(l.r,l.p1,l.r,l.p0):i(l.r,l.p1,c.r,c.p0)+r(c.r,c.p1,c.a1-c.a0)+i(c.r,c.p1,l.r,l.p0))+"Z"}function t(n,t,e,r){var i=t.call(n,e,r),u=a.call(n,i,r),o=l.call(n,i,r)-Io,f=c.call(n,i,r)-Io;return{r:u,a0:o,a1:f,p0:[u*Math.cos(o),u*Math.sin(o)],p1:[u*Math.cos(f),u*Math.sin(f)]}}function e(n,t){return n.a0==t.a0&&n.a1==t.a1}function r(n,t,e){return"A"+n+","+n+" 0 "+ +(e>Fo)+",1 "+t}function i(n,t,e,r){return"Q 0,0 "+r}var u=Me,o=xe,a=Iu,l=gu,c=vu;return n.radius=function(t){return arguments.length?(a=En(t),n):a},n.source=function(t){return arguments.length?(u=En(t),n):u},n.target=function(t){return arguments.length?(o=En(t),n):o},n.startAngle=function(t){return arguments.length?(l=En(t),n):l},n.endAngle=function(t){return arguments.length?(c=En(t),n):c},n},ao.svg.diagonal=function(){function n(n,i){var u=t.call(this,n,i),o=e.call(this,n,i),a=(u.y+o.y)/2,l=[u,{x:u.x,y:a},{x:o.x,y:a},o];return l=l.map(r),"M"+l[0]+"C"+l[1]+" "+l[2]+" "+l[3]}var t=Me,e=xe,r=Yu;return n.source=function(e){return arguments.length?(t=En(e),n):t},n.target=function(t){return arguments.length?(e=En(t),n):e},n.projection=function(t){return arguments.length?(r=t,n):r},n},ao.svg.diagonal.radial=function(){var n=ao.svg.diagonal(),t=Yu,e=n.projection;return n.projection=function(n){return arguments.length?e(Zu(t=n)):t},n},ao.svg.symbol=function(){function n(n,r){return(Ul.get(t.call(this,n,r))||$u)(e.call(this,n,r))}var t=Xu,e=Vu;return n.type=function(e){return arguments.length?(t=En(e),n):t},n.size=function(t){return arguments.length?(e=En(t),n):e},n};var Ul=ao.map({circle:$u,cross:function(n){var t=Math.sqrt(n/5)/2;return"M"+-3*t+","+-t+"H"+-t+"V"+-3*t+"H"+t+"V"+-t+"H"+3*t+"V"+t+"H"+t+"V"+3*t+"H"+-t+"V"+t+"H"+-3*t+"Z"},diamond:function(n){var t=Math.sqrt(n/(2*Fl)),e=t*Fl;return"M0,"+-t+"L"+e+",0 0,"+t+" "+-e+",0Z"},square:function(n){var t=Math.sqrt(n)/2;return"M"+-t+","+-t+"L"+t+","+-t+" "+t+","+t+" "+-t+","+t+"Z"},"triangle-down":function(n){var t=Math.sqrt(n/jl),e=t*jl/2;return"M0,"+e+"L"+t+","+-e+" "+-t+","+-e+"Z"},"triangle-up":function(n){var t=Math.sqrt(n/jl),e=t*jl/2;return"M0,"+-e+"L"+t+","+e+" "+-t+","+e+"Z"}});ao.svg.symbolTypes=Ul.keys();var jl=Math.sqrt(3),Fl=Math.tan(30*Yo);Co.transition=function(n){for(var t,e,r=Hl||++Zl,i=Ku(n),u=[],o=Ol||{time:Date.now(),ease:Nr,delay:0,duration:250},a=-1,l=this.length;++au;u++){i.push(t=[]);for(var e=this[u],a=0,l=e.length;l>a;a++)(r=e[a])&&n.call(r,r.__data__,a,u)&&t.push(r)}return Wu(i,this.namespace,this.id)},Yl.tween=function(n,t){var e=this.id,r=this.namespace;return arguments.length<2?this.node()[r][e].tween.get(n):Y(this,null==t?function(t){t[r][e].tween.remove(n)}:function(i){i[r][e].tween.set(n,t)})},Yl.attr=function(n,t){function e(){this.removeAttribute(a)}function r(){this.removeAttributeNS(a.space,a.local)}function i(n){return null==n?e:(n+="",function(){var t,e=this.getAttribute(a);return e!==n&&(t=o(e,n),function(n){this.setAttribute(a,t(n))})})}function u(n){return null==n?r:(n+="",function(){var t,e=this.getAttributeNS(a.space,a.local);return e!==n&&(t=o(e,n),function(n){this.setAttributeNS(a.space,a.local,t(n))})})}if(arguments.length<2){for(t in n)this.attr(t,n[t]);return this}var o="transform"==n?$r:Mr,a=ao.ns.qualify(n);return Ju(this,"attr."+n,t,a.local?u:i)},Yl.attrTween=function(n,t){function e(n,e){var r=t.call(this,n,e,this.getAttribute(i));return r&&function(n){this.setAttribute(i,r(n))}}function r(n,e){var r=t.call(this,n,e,this.getAttributeNS(i.space,i.local));return r&&function(n){this.setAttributeNS(i.space,i.local,r(n))}}var i=ao.ns.qualify(n);return this.tween("attr."+n,i.local?r:e)},Yl.style=function(n,e,r){function i(){this.style.removeProperty(n)}function u(e){return null==e?i:(e+="",function(){var i,u=t(this).getComputedStyle(this,null).getPropertyValue(n);return u!==e&&(i=Mr(u,e),function(t){this.style.setProperty(n,i(t),r)})})}var o=arguments.length;if(3>o){if("string"!=typeof n){2>o&&(e="");for(r in n)this.style(r,n[r],e);return this}r=""}return Ju(this,"style."+n,e,u)},Yl.styleTween=function(n,e,r){function i(i,u){var o=e.call(this,i,u,t(this).getComputedStyle(this,null).getPropertyValue(n));return o&&function(t){this.style.setProperty(n,o(t),r)}}return arguments.length<3&&(r=""),this.tween("style."+n,i)},Yl.text=function(n){return Ju(this,"text",n,Gu)},Yl.remove=function(){var n=this.namespace;return this.each("end.transition",function(){var t;this[n].count<2&&(t=this.parentNode)&&t.removeChild(this)})},Yl.ease=function(n){var t=this.id,e=this.namespace;return arguments.length<1?this.node()[e][t].ease:("function"!=typeof n&&(n=ao.ease.apply(ao,arguments)),Y(this,function(r){r[e][t].ease=n}))},Yl.delay=function(n){var t=this.id,e=this.namespace;return arguments.length<1?this.node()[e][t].delay:Y(this,"function"==typeof n?function(r,i,u){r[e][t].delay=+n.call(r,r.__data__,i,u)}:(n=+n,function(r){r[e][t].delay=n}))},Yl.duration=function(n){var t=this.id,e=this.namespace;return arguments.length<1?this.node()[e][t].duration:Y(this,"function"==typeof n?function(r,i,u){r[e][t].duration=Math.max(1,n.call(r,r.__data__,i,u))}:(n=Math.max(1,n),function(r){r[e][t].duration=n}))},Yl.each=function(n,t){var e=this.id,r=this.namespace;if(arguments.length<2){var i=Ol,u=Hl;try{Hl=e,Y(this,function(t,i,u){Ol=t[r][e],n.call(t,t.__data__,i,u)})}finally{Ol=i,Hl=u}}else Y(this,function(i){var u=i[r][e];(u.event||(u.event=ao.dispatch("start","end","interrupt"))).on(n,t)});return this},Yl.transition=function(){for(var n,t,e,r,i=this.id,u=++Zl,o=this.namespace,a=[],l=0,c=this.length;c>l;l++){a.push(n=[]);for(var t=this[l],f=0,s=t.length;s>f;f++)(e=t[f])&&(r=e[o][i],Qu(e,f,o,u,{time:r.time,ease:r.ease,delay:r.delay+r.duration,duration:r.duration})),n.push(e)}return Wu(a,o,u)},ao.svg.axis=function(){function n(n){n.each(function(){var n,c=ao.select(this),f=this.__chart__||e,s=this.__chart__=e.copy(),h=null==l?s.ticks?s.ticks.apply(s,a):s.domain():l,p=null==t?s.tickFormat?s.tickFormat.apply(s,a):m:t,g=c.selectAll(".tick").data(h,s),v=g.enter().insert("g",".domain").attr("class","tick").style("opacity",Uo),d=ao.transition(g.exit()).style("opacity",Uo).remove(),y=ao.transition(g.order()).style("opacity",1),M=Math.max(i,0)+o,x=Zi(s),b=c.selectAll(".domain").data([0]),_=(b.enter().append("path").attr("class","domain"),ao.transition(b));v.append("line"),v.append("text");var w,S,k,N,E=v.select("line"),A=y.select("line"),C=g.select("text").text(p),z=v.select("text"),L=y.select("text"),q="top"===r||"left"===r?-1:1;if("bottom"===r||"top"===r?(n=no,w="x",k="y",S="x2",N="y2",C.attr("dy",0>q?"0em":".71em").style("text-anchor","middle"),_.attr("d","M"+x[0]+","+q*u+"V0H"+x[1]+"V"+q*u)):(n=to,w="y",k="x",S="y2",N="x2",C.attr("dy",".32em").style("text-anchor",0>q?"end":"start"),_.attr("d","M"+q*u+","+x[0]+"H0V"+x[1]+"H"+q*u)),E.attr(N,q*i),z.attr(k,q*M),A.attr(S,0).attr(N,q*i),L.attr(w,0).attr(k,q*M),s.rangeBand){var T=s,R=T.rangeBand()/2;f=s=function(n){return T(n)+R}}else f.rangeBand?f=s:d.call(n,s,f);v.call(n,f,s),y.call(n,s,s)})}var t,e=ao.scale.linear(),r=Vl,i=6,u=6,o=3,a=[10],l=null;return n.scale=function(t){return arguments.length?(e=t,n):e},n.orient=function(t){return arguments.length?(r=t in Xl?t+"":Vl,n):r},n.ticks=function(){return arguments.length?(a=co(arguments),n):a},n.tickValues=function(t){return arguments.length?(l=t,n):l},n.tickFormat=function(e){return arguments.length?(t=e,n):t},n.tickSize=function(t){var e=arguments.length;return e?(i=+t,u=+arguments[e-1],n):i},n.innerTickSize=function(t){return arguments.length?(i=+t,n):i},n.outerTickSize=function(t){return arguments.length?(u=+t,n):u},n.tickPadding=function(t){return arguments.length?(o=+t,n):o},n.tickSubdivide=function(){return arguments.length&&n},n};var Vl="bottom",Xl={top:1,right:1,bottom:1,left:1};ao.svg.brush=function(){function n(t){t.each(function(){var t=ao.select(this).style("pointer-events","all").style("-webkit-tap-highlight-color","rgba(0,0,0,0)").on("mousedown.brush",u).on("touchstart.brush",u),o=t.selectAll(".background").data([0]);o.enter().append("rect").attr("class","background").style("visibility","hidden").style("cursor","crosshair"),t.selectAll(".extent").data([0]).enter().append("rect").attr("class","extent").style("cursor","move");var a=t.selectAll(".resize").data(v,m);a.exit().remove(),a.enter().append("g").attr("class",function(n){return"resize "+n}).style("cursor",function(n){return $l[n]}).append("rect").attr("x",function(n){return/[ew]$/.test(n)?-3:null}).attr("y",function(n){return/^[ns]/.test(n)?-3:null}).attr("width",6).attr("height",6).style("visibility","hidden"),a.style("display",n.empty()?"none":null);var l,s=ao.transition(t),h=ao.transition(o);c&&(l=Zi(c),h.attr("x",l[0]).attr("width",l[1]-l[0]),r(s)),f&&(l=Zi(f),h.attr("y",l[0]).attr("height",l[1]-l[0]),i(s)),e(s)})}function e(n){n.selectAll(".resize").attr("transform",function(n){return"translate("+s[+/e$/.test(n)]+","+h[+/^s/.test(n)]+")"})}function r(n){n.select(".extent").attr("x",s[0]),n.selectAll(".extent,.n>rect,.s>rect").attr("width",s[1]-s[0])}function i(n){n.select(".extent").attr("y",h[0]),n.selectAll(".extent,.e>rect,.w>rect").attr("height",h[1]-h[0])}function u(){function u(){32==ao.event.keyCode&&(C||(M=null,L[0]-=s[1],L[1]-=h[1],C=2),S())}function v(){32==ao.event.keyCode&&2==C&&(L[0]+=s[1],L[1]+=h[1],C=0,S())}function d(){var n=ao.mouse(b),t=!1;x&&(n[0]+=x[0],n[1]+=x[1]),C||(ao.event.altKey?(M||(M=[(s[0]+s[1])/2,(h[0]+h[1])/2]),L[0]=s[+(n[0]f?(i=r,r=f):i=f),v[0]!=r||v[1]!=i?(e?a=null:o=null,v[0]=r,v[1]=i,!0):void 0}function m(){d(),k.style("pointer-events","all").selectAll(".resize").style("display",n.empty()?"none":null),ao.select("body").style("cursor",null),q.on("mousemove.brush",null).on("mouseup.brush",null).on("touchmove.brush",null).on("touchend.brush",null).on("keydown.brush",null).on("keyup.brush",null),z(),w({type:"brushend"})}var M,x,b=this,_=ao.select(ao.event.target),w=l.of(b,arguments),k=ao.select(b),N=_.datum(),E=!/^(n|s)$/.test(N)&&c,A=!/^(e|w)$/.test(N)&&f,C=_.classed("extent"),z=W(b),L=ao.mouse(b),q=ao.select(t(b)).on("keydown.brush",u).on("keyup.brush",v);if(ao.event.changedTouches?q.on("touchmove.brush",d).on("touchend.brush",m):q.on("mousemove.brush",d).on("mouseup.brush",m),k.interrupt().selectAll("*").interrupt(),C)L[0]=s[0]-L[0],L[1]=h[0]-L[1];else if(N){var T=+/w$/.test(N),R=+/^n/.test(N);x=[s[1-T]-L[0],h[1-R]-L[1]],L[0]=s[T],L[1]=h[R]}else ao.event.altKey&&(M=L.slice());k.style("pointer-events","none").selectAll(".resize").style("display",null),ao.select("body").style("cursor",_.style("cursor")),w({type:"brushstart"}),d()}var o,a,l=N(n,"brushstart","brush","brushend"),c=null,f=null,s=[0,0],h=[0,0],p=!0,g=!0,v=Bl[0];return n.event=function(n){n.each(function(){var n=l.of(this,arguments),t={x:s,y:h,i:o,j:a},e=this.__chart__||t;this.__chart__=t,Hl?ao.select(this).transition().each("start.brush",function(){o=e.i,a=e.j,s=e.x,h=e.y,n({type:"brushstart"})}).tween("brush:brush",function(){var e=xr(s,t.x),r=xr(h,t.y);return o=a=null,function(i){s=t.x=e(i),h=t.y=r(i),n({type:"brush",mode:"resize"})}}).each("end.brush",function(){o=t.i,a=t.j,n({type:"brush",mode:"resize"}),n({type:"brushend"})}):(n({type:"brushstart"}),n({type:"brush",mode:"resize"}),n({type:"brushend"}))})},n.x=function(t){return arguments.length?(c=t,v=Bl[!c<<1|!f],n):c},n.y=function(t){return arguments.length?(f=t,v=Bl[!c<<1|!f],n):f},n.clamp=function(t){return arguments.length?(c&&f?(p=!!t[0],g=!!t[1]):c?p=!!t:f&&(g=!!t),n):c&&f?[p,g]:c?p:f?g:null},n.extent=function(t){var e,r,i,u,l;return arguments.length?(c&&(e=t[0],r=t[1],f&&(e=e[0],r=r[0]),o=[e,r],c.invert&&(e=c(e),r=c(r)),e>r&&(l=e,e=r,r=l),e==s[0]&&r==s[1]||(s=[e,r])),f&&(i=t[0],u=t[1],c&&(i=i[1],u=u[1]),a=[i,u],f.invert&&(i=f(i),u=f(u)),i>u&&(l=i,i=u,u=l),i==h[0]&&u==h[1]||(h=[i,u])),n):(c&&(o?(e=o[0],r=o[1]):(e=s[0],r=s[1],c.invert&&(e=c.invert(e),r=c.invert(r)),e>r&&(l=e,e=r,r=l))),f&&(a?(i=a[0],u=a[1]):(i=h[0],u=h[1],f.invert&&(i=f.invert(i),u=f.invert(u)),i>u&&(l=i,i=u,u=l))),c&&f?[[e,i],[r,u]]:c?[e,r]:f&&[i,u])},n.clear=function(){return n.empty()||(s=[0,0],h=[0,0],o=a=null),n},n.empty=function(){return!!c&&s[0]==s[1]||!!f&&h[0]==h[1]},ao.rebind(n,l,"on")};var $l={n:"ns-resize",e:"ew-resize",s:"ns-resize",w:"ew-resize",nw:"nwse-resize",ne:"nesw-resize",se:"nwse-resize",sw:"nesw-resize"},Bl=[["n","e","s","w","nw","ne","se","sw"],["e","w"],["n","s"],[]],Wl=ga.format=xa.timeFormat,Jl=Wl.utc,Gl=Jl("%Y-%m-%dT%H:%M:%S.%LZ");Wl.iso=Date.prototype.toISOString&&+new Date("2000-01-01T00:00:00.000Z")?eo:Gl,eo.parse=function(n){var t=new Date(n);return isNaN(t)?null:t},eo.toString=Gl.toString,ga.second=On(function(n){return new va(1e3*Math.floor(n/1e3))},function(n,t){n.setTime(n.getTime()+1e3*Math.floor(t))},function(n){return n.getSeconds()}),ga.seconds=ga.second.range,ga.seconds.utc=ga.second.utc.range,ga.minute=On(function(n){return new va(6e4*Math.floor(n/6e4))},function(n,t){n.setTime(n.getTime()+6e4*Math.floor(t))},function(n){return n.getMinutes()}),ga.minutes=ga.minute.range,ga.minutes.utc=ga.minute.utc.range,ga.hour=On(function(n){var t=n.getTimezoneOffset()/60;return new va(36e5*(Math.floor(n/36e5-t)+t))},function(n,t){n.setTime(n.getTime()+36e5*Math.floor(t))},function(n){return n.getHours()}),ga.hours=ga.hour.range,ga.hours.utc=ga.hour.utc.range,ga.month=On(function(n){return n=ga.day(n),n.setDate(1),n},function(n,t){n.setMonth(n.getMonth()+t)},function(n){return n.getMonth()}),ga.months=ga.month.range,ga.months.utc=ga.month.utc.range;var Kl=[1e3,5e3,15e3,3e4,6e4,3e5,9e5,18e5,36e5,108e5,216e5,432e5,864e5,1728e5,6048e5,2592e6,7776e6,31536e6],Ql=[[ga.second,1],[ga.second,5],[ga.second,15],[ga.second,30],[ga.minute,1],[ga.minute,5],[ga.minute,15],[ga.minute,30],[ga.hour,1],[ga.hour,3],[ga.hour,6],[ga.hour,12],[ga.day,1],[ga.day,2],[ga.week,1],[ga.month,1],[ga.month,3],[ga.year,1]],nc=Wl.multi([[".%L",function(n){return n.getMilliseconds()}],[":%S",function(n){return n.getSeconds()}],["%I:%M",function(n){return n.getMinutes()}],["%I %p",function(n){return n.getHours()}],["%a %d",function(n){return n.getDay()&&1!=n.getDate()}],["%b %d",function(n){return 1!=n.getDate()}],["%B",function(n){return n.getMonth()}],["%Y",zt]]),tc={range:function(n,t,e){return ao.range(Math.ceil(n/e)*e,+t,e).map(io)},floor:m,ceil:m};Ql.year=ga.year,ga.scale=function(){return ro(ao.scale.linear(),Ql,nc)};var ec=Ql.map(function(n){return[n[0].utc,n[1]]}),rc=Jl.multi([[".%L",function(n){return n.getUTCMilliseconds()}],[":%S",function(n){return n.getUTCSeconds()}],["%I:%M",function(n){return n.getUTCMinutes()}],["%I %p",function(n){return n.getUTCHours()}],["%a %d",function(n){return n.getUTCDay()&&1!=n.getUTCDate()}],["%b %d",function(n){return 1!=n.getUTCDate()}],["%B",function(n){return n.getUTCMonth()}],["%Y",zt]]);ec.year=ga.year.utc,ga.scale.utc=function(){return ro(ao.scale.linear(),ec,rc)},ao.text=An(function(n){return n.responseText}),ao.json=function(n,t){return Cn(n,"application/json",uo,t)},ao.html=function(n,t){return Cn(n,"text/html",oo,t)},ao.xml=An(function(n){return n.responseXML}),"function"==typeof define&&define.amd?(this.d3=ao,define(ao)):"object"==typeof module&&module.exports?module.exports=ao:this.d3=ao}();/*! Holder - client side image placeholders Version 2.7.1+6hydf © 2015 Ivan Malopinsky - http://imsky.co Site: http://holderjs.com Issues: https://github.com/imsky/holder/issues License: http://opensource.org/licenses/MIT */ !function(a){if(a.document){var b=a.document;b.querySelectorAll||(b.querySelectorAll=function(c){var d,e=b.createElement("style"),f=[];for(b.documentElement.firstChild.appendChild(e),b._qsa=[],e.styleSheet.cssText=c+"{x-qsa:expression(document._qsa && document._qsa.push(this))}",a.scrollBy(0,0),e.parentNode.removeChild(e);b._qsa.length;)d=b._qsa.shift(),d.style.removeAttribute("x-qsa"),f.push(d);return b._qsa=null,f}),b.querySelector||(b.querySelector=function(a){var c=b.querySelectorAll(a);return c.length?c[0]:null}),b.getElementsByClassName||(b.getElementsByClassName=function(a){return a=String(a).replace(/^|\s+/g,"."),b.querySelectorAll(a)}),Object.keys||(Object.keys=function(a){if(a!==Object(a))throw TypeError("Object.keys called on non-object");var b,c=[];for(b in a)Object.prototype.hasOwnProperty.call(a,b)&&c.push(b);return c}),function(a){var b="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";a.atob=a.atob||function(a){a=String(a);var c,d=0,e=[],f=0,g=0;if(a=a.replace(/\s/g,""),a.length%4===0&&(a=a.replace(/=+$/,"")),a.length%4===1)throw Error("InvalidCharacterError");if(/[^+/0-9A-Za-z]/.test(a))throw Error("InvalidCharacterError");for(;d>16&255)),e.push(String.fromCharCode(f>>8&255)),e.push(String.fromCharCode(255&f)),g=0,f=0),d+=1;return 12===g?(f>>=4,e.push(String.fromCharCode(255&f))):18===g&&(f>>=2,e.push(String.fromCharCode(f>>8&255)),e.push(String.fromCharCode(255&f))),e.join("")},a.btoa=a.btoa||function(a){a=String(a);var c,d,e,f,g,h,i,j=0,k=[];if(/[^\x00-\xFF]/.test(a))throw Error("InvalidCharacterError");for(;j>2,g=(3&c)<<4|d>>4,h=(15&d)<<2|e>>6,i=63&e,j===a.length+2?(h=64,i=64):j===a.length+1&&(i=64),k.push(b.charAt(f),b.charAt(g),b.charAt(h),b.charAt(i));return k.join("")}}(a),Object.prototype.hasOwnProperty||(Object.prototype.hasOwnProperty=function(a){var b=this.__proto__||this.constructor.prototype;return a in this&&(!(a in b)||b[a]!==this[a])}),function(){if("performance"in a==!1&&(a.performance={}),Date.now=Date.now||function(){return(new Date).getTime()},"now"in a.performance==!1){var b=Date.now();performance.timing&&performance.timing.navigationStart&&(b=performance.timing.navigationStart),a.performance.now=function(){return Date.now()-b}}}(),a.requestAnimationFrame||(a.webkitRequestAnimationFrame?!function(a){a.requestAnimationFrame=function(b){return webkitRequestAnimationFrame(function(){b(a.performance.now())})},a.cancelAnimationFrame=webkitCancelAnimationFrame}(a):a.mozRequestAnimationFrame?!function(a){a.requestAnimationFrame=function(b){return mozRequestAnimationFrame(function(){b(a.performance.now())})},a.cancelAnimationFrame=mozCancelAnimationFrame}(a):!function(a){a.requestAnimationFrame=function(b){return a.setTimeout(b,1e3/60)},a.cancelAnimationFrame=a.clearTimeout}(a))}}(this),function(a,b){"object"==typeof exports&&"object"==typeof module?module.exports=b():"function"==typeof define&&define.amd?define(b):"object"==typeof exports?exports.Holder=b():a.Holder=b()}(this,function(){return function(a){function b(d){if(c[d])return c[d].exports;var e=c[d]={exports:{},id:d,loaded:!1};return a[d].call(e.exports,e,e.exports,b),e.loaded=!0,e.exports}var c={};return b.m=a,b.c=c,b.p="",b(0)}([function(a,b,c){(function(b){function d(a,b,c,d){var f=e(c.substr(c.lastIndexOf(a.domain)),a);f&&h({mode:null,el:d,flags:f,engineSettings:b})}function e(a,b){var c={theme:B(J.settings.themes.gray,null),stylesheets:b.stylesheets,instanceOptions:b};return a.match(/([\d]+p?)x([\d]+p?)(?:\?|$)/)?f(a,c):g(a,c)}function f(a,b){var c=a.split("?"),d=c[0].split("/");b.holderURL=a;var e=d[1],f=e.match(/([\d]+p?)x([\d]+p?)/);if(!f)return!1;if(b.fluid=-1!==e.indexOf("p"),b.dimensions={width:f[1].replace("p","%"),height:f[2].replace("p","%")},2===c.length){var g=A.parse(c[1]);if(g.bg&&(b.theme.background=(-1===g.bg.indexOf("#")?"#":"")+g.bg),g.fg&&(b.theme.foreground=(-1===g.fg.indexOf("#")?"#":"")+g.fg),g.theme&&b.instanceOptions.themes.hasOwnProperty(g.theme)&&(b.theme=B(b.instanceOptions.themes[g.theme],null)),g.text&&(b.text=g.text),g.textmode&&(b.textmode=g.textmode),g.size&&(b.size=g.size),g.font&&(b.font=g.font),g.align&&(b.align=g.align),b.nowrap=z.truthy(g.nowrap),b.auto=z.truthy(g.auto),z.truthy(g.random)){J.vars.cache.themeKeys=J.vars.cache.themeKeys||Object.keys(b.instanceOptions.themes);var h=J.vars.cache.themeKeys[0|Math.random()*J.vars.cache.themeKeys.length];b.theme=B(b.instanceOptions.themes[h],null)}}return b}function g(a,b){var c=!1,d=String.fromCharCode(11),e=a.replace(/([^\\])\//g,"$1"+d).split(d),f=/%[0-9a-f]{2}/gi,g=b.instanceOptions;b.holderURL=[];for(var h=e.length,i=0;h>i;i++){var j=e[i];if(j.match(f))try{j=decodeURIComponent(j)}catch(k){j=e[i]}var l=!1;if(J.flags.dimensions.match(j))c=!0,b.dimensions=J.flags.dimensions.output(j),l=!0;else if(J.flags.fluid.match(j))c=!0,b.dimensions=J.flags.fluid.output(j),b.fluid=!0,l=!0;else if(J.flags.textmode.match(j))b.textmode=J.flags.textmode.output(j),l=!0;else if(J.flags.colors.match(j)){var m=J.flags.colors.output(j);b.theme=B(b.theme,m),l=!0}else if(g.themes[j])g.themes.hasOwnProperty(j)&&(b.theme=B(g.themes[j],null)),l=!0;else if(J.flags.font.match(j))b.font=J.flags.font.output(j),l=!0;else if(J.flags.auto.match(j))b.auto=!0,l=!0;else if(J.flags.text.match(j))b.text=J.flags.text.output(j),l=!0;else if(J.flags.size.match(j))b.size=J.flags.size.output(j),l=!0;else if(J.flags.random.match(j)){null==J.vars.cache.themeKeys&&(J.vars.cache.themeKeys=Object.keys(g.themes));var n=J.vars.cache.themeKeys[0|Math.random()*J.vars.cache.themeKeys.length];b.theme=B(g.themes[n],null),l=!0}l&&b.holderURL.push(j)}return b.holderURL.unshift(g.domain),b.holderURL=b.holderURL.join("/"),c?b:!1}function h(a){var b=a.mode,c=a.el,d=a.flags,e=a.engineSettings,f=d.dimensions,g=d.theme,h=f.width+"x"+f.height;if(b=null==b?d.fluid?"fluid":"image":b,null!=d.text&&(g.text=d.text,"object"===c.nodeName.toLowerCase())){for(var j=g.text.split("\\n"),k=0;k1){var n,o=0,p=0,q=0;j=new e.Group("line"+q),("left"===a.align||"right"===a.align)&&(m=a.width*(1-2*(1-J.setup.lineWrapRatio)));for(var r=0;r=m||t===!0)&&(b(g,j,o,g.properties.leading),g.add(j),o=0,p+=g.properties.leading,q+=1,j=new e.Group("line"+q),j.y=p),t!==!0&&(i.moveTo(o,0),o+=h.spaceWidth+s.width,j.add(i))}if(b(g,j,o,g.properties.leading),g.add(j),"left"===a.align)g.moveTo(a.width-l,null,null);else if("right"===a.align){for(n in g.children)j=g.children[n],j.moveTo(a.width-j.width,null,null);g.moveTo(0-(a.width-l),null,null)}else{for(n in g.children)j=g.children[n],j.moveTo((g.width-j.width)/2,null,null);g.moveTo((a.width-g.width)/2,null,null)}g.moveTo(null,(a.height-g.height)/2,null),(a.height-g.height)/2<0&&g.moveTo(null,0,null)}else i=new e.Text(a.text),j=new e.Group("line0"),j.add(i),g.add(j),"left"===a.align?g.moveTo(a.width-l,null,null):"right"===a.align?g.moveTo(0-(a.width-l),null,null):g.moveTo((a.width-h.boundingBox.width)/2,null,null),g.moveTo(null,(a.height-h.boundingBox.height)/2,null);return d}function k(a,b,c){var d=parseInt(a,10),e=parseInt(b,10),f=Math.max(d,e),g=Math.min(d,e),h=.8*Math.min(g,f*J.defaults.scale);return Math.round(Math.max(c,h))}function l(a){var b;b=null==a||null==a.nodeType?J.vars.resizableImages:[a];for(var c=0,d=b.length;d>c;c++){var e=b[c];if(e.holderData){var f=e.holderData.flags,g=D(e);if(g){if(!e.holderData.resizeUpdate)continue;if(f.fluid&&f.auto){var h=e.holderData.fluidConfig;switch(h.mode){case"width":g.height=g.width/h.ratio;break;case"height":g.width=g.height*h.ratio}}var j={mode:"image",holderSettings:{dimensions:g,theme:f.theme,flags:f},el:e,engineSettings:e.holderData.engineSettings};"exact"==f.textmode&&(f.exactDimensions=g,j.holderSettings.dimensions=f.dimensions),i(j)}else p(e)}}}function m(a){if(a.holderData){var b=D(a);if(b){var c=a.holderData.flags,d={fluidHeight:"%"==c.dimensions.height.slice(-1),fluidWidth:"%"==c.dimensions.width.slice(-1),mode:null,initialDimensions:b};d.fluidWidth&&!d.fluidHeight?(d.mode="width",d.ratio=d.initialDimensions.width/parseFloat(c.dimensions.height)):!d.fluidWidth&&d.fluidHeight&&(d.mode="height",d.ratio=parseFloat(c.dimensions.width)/d.initialDimensions.height),a.holderData.fluidConfig=d}else p(a)}}function n(){for(var a,c=[],d=Object.keys(J.vars.invisibleImages),e=0,f=d.length;f>e;e++)a=J.vars.invisibleImages[d[e]],D(a)&&"img"==a.nodeName.toLowerCase()&&(c.push(a),delete J.vars.invisibleImages[d[e]]);c.length&&I.run({images:c}),b.requestAnimationFrame(n)}function o(){J.vars.visibilityCheckStarted||(b.requestAnimationFrame(n),J.vars.visibilityCheckStarted=!0)}function p(a){a.holderData.invisibleId||(J.vars.invisibleId+=1,J.vars.invisibleImages["i"+J.vars.invisibleId]=a,a.holderData.invisibleId=J.vars.invisibleId)}function q(a,b){return null==b?document.createElement(a):document.createElementNS(b,a)}function r(a,b){for(var c in b)a.setAttribute(c,b[c])}function s(a,b,c){var d,e;null==a?(a=q("svg",E),d=q("defs",E),e=q("style",E),r(e,{type:"text/css"}),d.appendChild(e),a.appendChild(d)):e=a.querySelector("style"),a.webkitMatchesSelector&&a.setAttribute("xmlns",E);for(var f=0;f=0;h--){var i=g.createProcessingInstruction("xml-stylesheet",'href="'+f[h]+'" rel="stylesheet"');g.insertBefore(i,g.firstChild)}g.removeChild(g.documentElement),e=d.serializeToString(g)}var j=d.serializeToString(a);return j=j.replace(/\&(\#[0-9]{2,}\;)/g,"&$1"),e+j}}function u(){return b.DOMParser?(new DOMParser).parseFromString("","application/xml"):void 0}function v(a){J.vars.debounceTimer||a.call(this),J.vars.debounceTimer&&b.clearTimeout(J.vars.debounceTimer),J.vars.debounceTimer=b.setTimeout(function(){J.vars.debounceTimer=null,a.call(this)},J.setup.debounce)}function w(){v(function(){l(null)})}var x=c(1),y=c(2),z=c(3),A=c(4),B=z.extend,C=z.getNodeArray,D=z.dimensionCheck,E="http://www.w3.org/2000/svg",F=8,G="2.7.1",H="\nCreated with Holder.js "+G+".\nLearn more at http://holderjs.com\n(c) 2012-2015 Ivan Malopinsky - http://imsky.co\n",I={version:G,addTheme:function(a,b){return null!=a&&null!=b&&(J.settings.themes[a]=b),delete J.vars.cache.themeKeys,this},addImage:function(a,b){var c=document.querySelectorAll(b);if(c.length)for(var d=0,e=c.length;e>d;d++){var f=q("img"),g={};g[J.vars.dataAttr]=a,r(f,g),c[d].appendChild(f)}return this},setResizeUpdate:function(a,b){a.holderData&&(a.holderData.resizeUpdate=!!b,a.holderData.resizeUpdate&&l(a))},run:function(a){a=a||{};var c={},f=B(J.settings,a);J.vars.preempted=!0,J.vars.dataAttr=f.dataAttr||J.vars.dataAttr,c.renderer=f.renderer?f.renderer:J.setup.renderer,-1===J.setup.renderers.join(",").indexOf(c.renderer)&&(c.renderer=J.setup.supportsSVG?"svg":J.setup.supportsCanvas?"canvas":"html");var g=C(f.images),i=C(f.bgnodes),j=C(f.stylenodes),k=C(f.objects);c.stylesheets=[],c.svgXMLStylesheet=!0,c.noFontFallback=f.noFontFallback?f.noFontFallback:!1;for(var l=0;l1){c.nodeValue="";for(var u=0;u=0?b:1)}function f(a){v?e(a):w.push(a)}null==document.readyState&&document.addEventListener&&(document.addEventListener("DOMContentLoaded",function y(){document.removeEventListener("DOMContentLoaded",y,!1),document.readyState="complete"},!1),document.readyState="loading");var g=a.document,h=g.documentElement,i="load",j=!1,k="on"+i,l="complete",m="readyState",n="attachEvent",o="detachEvent",p="addEventListener",q="DOMContentLoaded",r="onreadystatechange",s="removeEventListener",t=p in g,u=j,v=j,w=[];if(g[m]===l)e(b);else if(t)g[p](q,c,j),a[p](i,c,j);else{g[n](r,c),a[n](k,c);try{u=null==a.frameElement&&h}catch(x){}u&&u.doScroll&&!function z(){if(!v){try{u.doScroll("left")}catch(a){return e(z,50)}d(),b()}}()}return f.version="1.4.0",f.isReady=function(){return v},f}a.exports="undefined"!=typeof window&&b(window)},function(a,b,c){var d=c(5),e=function(a){function b(a,b){for(var c in b)a[c]=b[c];return a}var c=1,e=d.defclass({constructor:function(a){c++,this.parent=null,this.children={},this.id=c,this.name="n"+c,null!=a&&(this.name=a),this.x=0,this.y=0,this.z=0,this.width=0,this.height=0},resize:function(a,b){null!=a&&(this.width=a),null!=b&&(this.height=b)},moveTo:function(a,b,c){this.x=null!=a?a:this.x,this.y=null!=b?b:this.y,this.z=null!=c?c:this.z},add:function(a){var b=a.name;if(null!=this.children[b])throw"SceneGraph: child with that name already exists: "+b;this.children[b]=a,a.parent=this}}),f=d(e,function(b){this.constructor=function(){b.constructor.call(this,"root"),this.properties=a}}),g=d(e,function(a){function c(c,d){if(a.constructor.call(this,c),this.properties={fill:"#000"},null!=d)b(this.properties,d);else if(null!=c&&"string"!=typeof c)throw"SceneGraph: invalid node name"}this.Group=d.extend(this,{constructor:c,type:"group"}),this.Rect=d.extend(this,{constructor:c,type:"rect"}),this.Text=d.extend(this,{constructor:function(a){c.call(this),this.properties.text=a},type:"text"})}),h=new f;return this.Shape=g,this.root=h,this};a.exports=e},function(a,b){(function(a){b.extend=function(a,b){var c={};for(var d in a)a.hasOwnProperty(d)&&(c[d]=a[d]);if(null!=b)for(var e in b)b.hasOwnProperty(e)&&(c[e]=b[e]);return c},b.cssProps=function(a){var b=[];for(var c in a)a.hasOwnProperty(c)&&b.push(c+":"+a[c]);return b.join(";")},b.encodeHtmlEntity=function(a){for(var b=[],c=0,d=a.length-1;d>=0;d--)c=a.charCodeAt(d),b.unshift(c>128?["&#",c,";"].join(""):a[d]);return b.join("")},b.getNodeArray=function(b){var c=null;return"string"==typeof b?c=document.querySelectorAll(b):a.NodeList&&b instanceof a.NodeList?c=b:a.Node&&b instanceof a.Node?c=[b]:a.HTMLCollection&&b instanceof a.HTMLCollection?c=b:b instanceof Array?c=b:null===b&&(c=[]),c},b.imageExists=function(a,b){var c=new Image;c.onerror=function(){b.call(this,!1)},c.onload=function(){b.call(this,!0)},c.src=a},b.decodeHtmlEntity=function(a){return a.replace(/&#(\d+);/g,function(a,b){return String.fromCharCode(b)})},b.dimensionCheck=function(a){var b={height:a.clientHeight,width:a.clientWidth};return b.height&&b.width?b:!1},b.truthy=function(a){return"string"==typeof a?"true"===a||"yes"===a||"1"===a||"on"===a||"✓"===a:!!a}}).call(b,function(){return this}())},function(a,b,c){var d=encodeURIComponent,e=decodeURIComponent,f=c(6),g=c(7),h=/(\w+)\[(\d+)\]/,i=/\w+\.\w+/;b.parse=function(a){if("string"!=typeof a)return{};if(a=f(a),""===a)return{};"?"===a.charAt(0)&&(a=a.slice(1));for(var b={},c=a.split("&"),d=0;d