Backport integration tests

Signed-off-by: Lukas Reschke <lukas@statuscode.ch>
This commit is contained in:
Lukas Reschke 2017-01-19 01:59:08 +01:00
parent 37b9ddf67b
commit 55b2023b12
No known key found for this signature in database
GPG key ID: B9F6980CF6E759B1
2036 changed files with 178298 additions and 30 deletions

View file

@ -6,7 +6,7 @@ pipeline:
image: nextcloudci/php5.6:php5.6-3
environment:
- APP_NAME=user_saml
- CORE_BRANCH=master
- CORE_BRANCH=stable11
- DB=sqlite
commands:
# Pre-setup steps
@ -25,7 +25,7 @@ pipeline:
image: nextcloudci/php7.0:php7.0-2
environment:
- APP_NAME=user_saml
- CORE_BRANCH=master
- CORE_BRANCH=stable11
- DB=sqlite
commands:
# Pre-setup steps
@ -40,7 +40,7 @@ pipeline:
image: nextcloudci/php5.6:php5.6-3
environment:
- APP_NAME=user_saml
- CORE_BRANCH=master
- CORE_BRANCH=stable11
- DB=sqlite
commands:
- apt update && apt-get -y install php5-xdebug
@ -51,7 +51,7 @@ pipeline:
- cd ../server/apps/$APP_NAME
# Run phpunit tests
- cd tests/
- cd tests/unit/
- phpunit --configuration phpunit.xml
# Create coverage report
@ -65,7 +65,7 @@ pipeline:
image: nextcloudci/php7.0:php7.0-2
environment:
- APP_NAME=user_saml
- CORE_BRANCH=master
- CORE_BRANCH=stable11
- DB=sqlite
commands:
# Pre-setup steps
@ -74,37 +74,36 @@ pipeline:
- cd ../server/apps/$APP_NAME
# Run phpunit tests
- cd tests/
- cd tests/unit/
- phpunit --configuration phpunit.xml
when:
matrix:
TESTS: php7.0
php7.1:
image: nextcloudci/php7.1:php7.1-3
integration-tests:
image: nextcloudci/user_saml_shibboleth:user_saml_shibboleth-5
environment:
- APP_NAME=user_saml
- CORE_BRANCH=master
- DB=sqlite
- CORE_BRANCH=stable11
commands:
# FIXME: Move into Docker image
- yum -y install wget
# Pre-setup steps
- wget https://raw.githubusercontent.com/nextcloud/travis_ci/master/before_install.sh
- bash ./before_install.sh $APP_NAME $CORE_BRANCH $DB
- cd ../server/apps/$APP_NAME
# Run phpunit tests
- cd tests/
- phpunit --configuration phpunit.xml
- /start.sh &
- sleep 3
- scl enable rh-php56 bash
- rm -rf /var/www/html
- cd /var/www/
- git clone --depth 1 -b $CORE_BRANCH https://github.com/nextcloud/server html
- cd /var/www/html && git submodule update --init
- cd /var/www/html/apps/ && git clone -b $DRONE_BRANCH https://github.com/nextcloud/user_saml.git
- php /var/www/html/occ maintenance:install --database sqlite --admin-pass password
- php /var/www/html/occ app:enable user_saml
- chown -R apache:apache /var/www/html/
- cd /var/www/html/apps/user_saml/tests/integration && vendor/bin/behat
when:
matrix:
TESTS: php7.1
TESTS: integration-tests
matrix:
include:
- TESTS: php5.6
- TESTS: php7.0
- TESTS: php7.1
- TESTS: check-app-compatbility
- TESTS: signed-off-check
- TESTS: signed-off-check
- TESTS: integration-tests

View file

@ -0,0 +1,6 @@
{
"require-dev": {
"behat/behat": "^3.3",
"guzzlehttp/guzzle": "^6.2"
}
}

1076
tests/integration/composer.lock generated Executable file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,27 @@
Feature: EnvironmentVariable
Scenario: Authenticating using environment variable with SSO and no check if user exists on backend
And The setting "type" is set to "environment-variable"
And The setting "general-uid_mapping" is set to "REMOTE_USER"
And The environment variable "REMOTE_USER" is set to "not-provisioned-user"
When I send a GET request to "http://localhost/index.php/login"
Then I should be redirected to "http://localhost/index.php/apps/files/"
Then I should be logged-in to Nextcloud as user "not-provisioned-user"
Scenario: Authenticating using environment variable with SSO and successful check if user exists on backend
Given A local user with uid "provisioned-user" exists
And The setting "type" is set to "environment-variable"
And The setting "general-require_provisioned_account" is set to "1"
And The setting "general-uid_mapping" is set to "REMOTE_USER"
And The environment variable "REMOTE_USER" is set to "provisioned-user"
When I send a GET request to "http://localhost/index.php/login"
Then I should be redirected to "http://localhost/index.php/apps/files/"
Then I should be logged-in to Nextcloud as user "provisioned-user"
Scenario: Authenticating using environment variable with SSO and unsuccessful check if user exists on backend
Given The setting "type" is set to "environment-variable"
And The setting "general-require_provisioned_account" is set to "1"
And The setting "general-uid_mapping" is set to "REMOTE_USER"
And The environment variable "REMOTE_USER" is set to "certainly-not-provisioned-user"
When I send a GET request to "http://localhost/index.php/login"
Then I should be redirected to "http://localhost/index.php/apps/user_saml/saml/notProvisioned"

View file

@ -0,0 +1,63 @@
Feature: Shibboleth
Scenario: Authenticating using Shibboleth with SAML and check if user exists on backend and not existing user
Given The setting "type" is set to "saml"
And The setting "general-require_provisioned_account" is set to "1"
And The setting "general-uid_mapping" is set to "urn:oid:0.9.2342.19200300.100.1.1"
And The setting "idp-entityId" is set to "https://shibboleth-integration-nextcloud.localdomain/idp/shibboleth"
And The setting "idp-singleSignOnService.url" is set to "https://localhost:4443/idp/profile/SAML2/Redirect/SSO"
And The setting "idp-x509cert" is set to "MIIDnTCCAoWgAwIBAgIUGPx9uPjCu7c172IUgV84Tm94pBcwDQYJKoZIhvcNAQEL BQAwNzE1MDMGA1UEAwwsc2hpYmJvbGV0aC1pbnRlZ3JhdGlvbi1uZXh0Y2xvdWQu bG9jYWxkb21haW4wHhcNMTcwMTA0MTAxMTI3WhcNMzcwMTA0MTAxMTI3WjA3MTUw MwYDVQQDDCxzaGliYm9sZXRoLWludGVncmF0aW9uLW5leHRjbG91ZC5sb2NhbGRv bWFpbjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKH+bPu45tk8/JRk XOxkyfbxocWZlY4mRumEUxscd3fn0oVzOrdWbHH7lCZV4bus4KxvJljc0Nm2K+Zr LoiRUUnf/LQ4LlehWVm5Kbc4kRgOXS0iGZN3SslAWPKyIg0tywg+TLOBPoS6EtST 1WuYg1JPMFxPfeFDWQ0dQYPlXIJWBFh6F2JMTb0FLECqA5l/ryYE13QisX5l+Mqo 6y3Dh7qIgaH0IJNobXoAcEWza7Kb2RnfhZRh9e0qjZIwBqTJUFM/6I86RYXn829s INUvYQQbez6VkGTdUQJ/GuXb/dD5sMQfOyK8hrRY5MozOmK32cz3JaAzSXpiSRS9 NxFwvicCAwEAAaOBoDCBnTAdBgNVHQ4EFgQUKn8+TV0WXSDeavvF0M8mWn1o8ukw fAYDVR0RBHUwc4Isc2hpYmJvbGV0aC1pbnRlZ3JhdGlvbi1uZXh0Y2xvdWQubG9j YWxkb21haW6GQ2h0dHBzOi8vc2hpYmJvbGV0aC1pbnRlZ3JhdGlvbi1uZXh0Y2xv dWQubG9jYWxkb21haW4vaWRwL3NoaWJib2xldGgwDQYJKoZIhvcNAQELBQADggEB ABI6uzoIeLZT9Az2KTlLxIc6jZ4MDmhaVja4ZuBxTXEb7BFLfeASEJmQm1wgIMOn pJId3Kh3njW+3tOBWKm7lj8JxVVpAu4yMFSoQGPaVUgYB1AVm+pmAyPLzfJ/XGhf esCU2F/b0eHWcaIb3x+BZFX089Cd/PBtP84MNXdo+TccibxC8N39sr45qJM/7SC7 TfDYU0L4q2WZHJr4S7+0F+F4KaxLx9NzCvN4h6XaoWofZWir2iHO4NzbrVQGC0ei QybS/neBfni4A2g1lyzCb6xFB58JBvNCn7AAnDJULOE7S5XWUKsDAQVQrxTNkUq7 pnhlCQqZDwUdgmIXd1KB1So="
And The setting "security-authnRequestsSigned" is set to "1"
And The setting "security-wantAssertionsEncrypted" is set to "1"
And The setting "sp-x509cert" is set to "-----BEGIN CERTIFICATE-----MIIC+zCCAeOgAwIBAgIJAIgZuvWDBIrdMA0GCSqGSIb3DQEBBQUAMBQxEjAQBgNVBAMMCWxvY2FsaG9zdDAeFw0xNzAxMDQxMTM5MjFaFw0yNzAxMDIxMTM5MjFaMBQxEjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAN3ESWaDH1JiJTy9yRJQV7kahPOxgBkIH2xwcYDL1k9deKNhSKLx7aGfxE244+HBcC6WLHKVUnOm0ld2qxQ4bMYiJXzZuqL67r07L5wxGAssv12lO92qohGmlHy3+VzRYUBmovu6upqOv3R2F8HBbo7Jc7Hvt7hOEJn/jPuFuF/fHit3mqU8l6IkrIZjpaW8T9fIWOXRq98U4+hkgWpqEZWsqlfE8BxAs9DeIMZab0GxO9stHLp+GYKx10uE4ezFcaDS8W+g2C8enCTt1HXGvcnj4o5zkC1lITGvcFTsiFqfIWyXeSufcxdc0W7HoG6J3ks0WJyK38sfFn0t2Ao6kX0CAwEAAaNQME4wHQYDVR0OBBYEFAoJzX6TVYAwC1GSPe6nObBG54zaMB8GA1UdIwQYMBaAFAoJzX6TVYAwC1GSPe6nObBG54zaMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAJia9R70uXdUZtgujUPjLas4+sVajzlBFmqhBqpLAo934vljf9HISsHrPtdBcbS0d0rucqXwabDf0MlR18ksnT/NYpsTwMbMx76CrXi4zYEEW5lISKEO65aIkzVTcqKWSuhjtSnRdB6iOLsFiKmNMWXaIKMR5T0+AbR9wdQgn08W+3EEeHGvafVQfE3STVsSgNb1ft7DvcSUnfPXGU7KzvmTpZa0Hfmc7uY4vpdEEhLAdRhgLReS7USZskov7ooiPSoD+JRFi2gM4klBxTemHdNUa9oFnHMXuYKOkLbkgFvHxyy+QlLq2ELQTga5e7I83ZyOfGctyf8Ul6vGw10vbQ=-----END CERTIFICATE-----"
And The setting "sp-privateKey" is set to "-----BEGIN RSA PRIVATE KEY-----MIIEpAIBAAKCAQEA3cRJZoMfUmIlPL3JElBXuRqE87GAGQgfbHBxgMvWT114o2FIovHtoZ/ETbjj4cFwLpYscpVSc6bSV3arFDhsxiIlfNm6ovruvTsvnDEYCyy/XaU73aqiEaaUfLf5XNFhQGai+7q6mo6/dHYXwcFujslzse+3uE4Qmf+M+4W4X98eK3eapTyXoiSshmOlpbxP18hY5dGr3xTj6GSBamoRlayqV8TwHECz0N4gxlpvQbE72y0cun4ZgrHXS4Th7MVxoNLxb6DYLx6cJO3Udca9yePijnOQLWUhMa9wVOyIWp8hbJd5K59zF1zRbsegboneSzRYnIrfyx8WfS3YCjqRfQIDAQABAoIBAQC5CQAdcqZ9vLpJNilBCJxJLCFmm+HAAREHD8MErg9A5UK1P4S1wJp/0qieGPi68wXBOTgY2xKSwMycgb04/+NyZidVRu388takOW/+KNBg8pMxdZ6/05GqnI0kivSbR3CXpYuz8hekwhpo9+fWmKjApsHL47ItK6WaeKmPbAFsq1YJGzfp/DXg7LIvh9GA3C1LWWGV7SuCGOyX/2Moi8xRa7qBtH4hDo/0NRhTx7zjYjlBgNEr330pJUopc3+AtHE40R+xMr2zkGvq9RsCZxYxD2VWbLwQW0yNjWmQ2OTuMgJJvk2+N73QLHcB+tea82ZTszsNzRS9DLtc6qbsKEPZAoGBAO78U3vEuRyY56f/1hpo0xuCDwOkWGzgBQWkjJl6dlyVz/zKkhXBHpEYImyt8XRN0W3iGZYpZ2hCFJGTcDp32R6UiEyGLz0Uc8R/tva/TiRVW1FdNczzSHcB24b9OMK4vE9JLs8mA8Rp8YBgtLr5DDuMfYt/a/rZJbg/HIfIN98nAoGBAO2OInCX93t2I6zzRPIqKtI6q6FYNp64VIQjvw9Y8l0x3IdJZRP9H5C8ZhCeYPsgEqTXcXa4j5hL4rQzoUtxfxflBUUH60bcnd4LGaTCMYLS14G011E3GZlIP0sJi5OjEhy8fq3zt6jVzS9V/lPHB8i+w1D7CbPrMpW7B3k32vC7AoGAX/HvdkYhZyjAAEOG6m1hK68IZhbp5TP+8CgCxm9S65K9wKh3A8LXibrdvzIKOP4w8WOPkCipOkMlTNibeu24vj01hztr5aK7Y40+oEtnjNCz67N3MQQO+LBHOSeaTRqrh01DPKjvZECAU2D/zfzEe3fIw2Nxr3DUYub7hkvMmosCgYAzxbVVypjiLGYsDDyrdmsstCKxoDMPNmcdAVljc+QmUXaZeXJw/8qAVb78wjeqo1vM1zNgR2rsKyW2VkZB1fN39q7GU6qAIBa7zLmDAduegmr7VrlSduq6UFeS9/qWa4TIBICrUqFlR2tXdKtgANF+e6y/mmaL8qdsoH1JetXZfwKBgQC1vscRpdAXivjOOZAh+mzJWzS4BUl4CTJLYYIuOEXikmN5g0EdV2fhUEdkewmyKnXHsd0x83167bYgpTDNs71jUxDHy5NXlg2qIjLkf09X9wr19gBzDApfWzfh3vUqttyMZuQMLVNepGCWM2vjlY9KGl5OvZqY6d+7yO0mLV9GmQ==-----END RSA PRIVATE KEY-----"
And The setting "security-wantAssertionsSigned" is set to "1"
When I send a GET request to "http://localhost/index.php/login"
Then I should be redirected to "https://localhost:4443/idp/profile/SAML2/Redirect/SSO"
And I send a POST request to "https://localhost:4443/idp/profile/SAML2/Redirect/SSO?execution=e1s1" with the following data
|j_username|j_password|_eventId_proceed|
|student1 |password | |
And The response should be a SAML redirect page that gets submitted
And I should be redirected to "http://localhost/index.php/apps/user_saml/saml/notProvisioned"
Scenario: Authenticating using Shibboleth with SAML and no check if user exists on backend
Given The setting "type" is set to "saml"
And The setting "general-uid_mapping" is set to "urn:oid:0.9.2342.19200300.100.1.1"
And The setting "idp-entityId" is set to "https://shibboleth-integration-nextcloud.localdomain/idp/shibboleth"
And The setting "idp-singleSignOnService.url" is set to "https://localhost:4443/idp/profile/SAML2/Redirect/SSO"
And The setting "idp-x509cert" is set to "MIIDnTCCAoWgAwIBAgIUGPx9uPjCu7c172IUgV84Tm94pBcwDQYJKoZIhvcNAQEL BQAwNzE1MDMGA1UEAwwsc2hpYmJvbGV0aC1pbnRlZ3JhdGlvbi1uZXh0Y2xvdWQu bG9jYWxkb21haW4wHhcNMTcwMTA0MTAxMTI3WhcNMzcwMTA0MTAxMTI3WjA3MTUw MwYDVQQDDCxzaGliYm9sZXRoLWludGVncmF0aW9uLW5leHRjbG91ZC5sb2NhbGRv bWFpbjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKH+bPu45tk8/JRk XOxkyfbxocWZlY4mRumEUxscd3fn0oVzOrdWbHH7lCZV4bus4KxvJljc0Nm2K+Zr LoiRUUnf/LQ4LlehWVm5Kbc4kRgOXS0iGZN3SslAWPKyIg0tywg+TLOBPoS6EtST 1WuYg1JPMFxPfeFDWQ0dQYPlXIJWBFh6F2JMTb0FLECqA5l/ryYE13QisX5l+Mqo 6y3Dh7qIgaH0IJNobXoAcEWza7Kb2RnfhZRh9e0qjZIwBqTJUFM/6I86RYXn829s INUvYQQbez6VkGTdUQJ/GuXb/dD5sMQfOyK8hrRY5MozOmK32cz3JaAzSXpiSRS9 NxFwvicCAwEAAaOBoDCBnTAdBgNVHQ4EFgQUKn8+TV0WXSDeavvF0M8mWn1o8ukw fAYDVR0RBHUwc4Isc2hpYmJvbGV0aC1pbnRlZ3JhdGlvbi1uZXh0Y2xvdWQubG9j YWxkb21haW6GQ2h0dHBzOi8vc2hpYmJvbGV0aC1pbnRlZ3JhdGlvbi1uZXh0Y2xv dWQubG9jYWxkb21haW4vaWRwL3NoaWJib2xldGgwDQYJKoZIhvcNAQELBQADggEB ABI6uzoIeLZT9Az2KTlLxIc6jZ4MDmhaVja4ZuBxTXEb7BFLfeASEJmQm1wgIMOn pJId3Kh3njW+3tOBWKm7lj8JxVVpAu4yMFSoQGPaVUgYB1AVm+pmAyPLzfJ/XGhf esCU2F/b0eHWcaIb3x+BZFX089Cd/PBtP84MNXdo+TccibxC8N39sr45qJM/7SC7 TfDYU0L4q2WZHJr4S7+0F+F4KaxLx9NzCvN4h6XaoWofZWir2iHO4NzbrVQGC0ei QybS/neBfni4A2g1lyzCb6xFB58JBvNCn7AAnDJULOE7S5XWUKsDAQVQrxTNkUq7 pnhlCQqZDwUdgmIXd1KB1So="
And The setting "security-authnRequestsSigned" is set to "1"
And The setting "security-wantAssertionsEncrypted" is set to "1"
And The setting "sp-x509cert" is set to "-----BEGIN CERTIFICATE-----MIIC+zCCAeOgAwIBAgIJAIgZuvWDBIrdMA0GCSqGSIb3DQEBBQUAMBQxEjAQBgNVBAMMCWxvY2FsaG9zdDAeFw0xNzAxMDQxMTM5MjFaFw0yNzAxMDIxMTM5MjFaMBQxEjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAN3ESWaDH1JiJTy9yRJQV7kahPOxgBkIH2xwcYDL1k9deKNhSKLx7aGfxE244+HBcC6WLHKVUnOm0ld2qxQ4bMYiJXzZuqL67r07L5wxGAssv12lO92qohGmlHy3+VzRYUBmovu6upqOv3R2F8HBbo7Jc7Hvt7hOEJn/jPuFuF/fHit3mqU8l6IkrIZjpaW8T9fIWOXRq98U4+hkgWpqEZWsqlfE8BxAs9DeIMZab0GxO9stHLp+GYKx10uE4ezFcaDS8W+g2C8enCTt1HXGvcnj4o5zkC1lITGvcFTsiFqfIWyXeSufcxdc0W7HoG6J3ks0WJyK38sfFn0t2Ao6kX0CAwEAAaNQME4wHQYDVR0OBBYEFAoJzX6TVYAwC1GSPe6nObBG54zaMB8GA1UdIwQYMBaAFAoJzX6TVYAwC1GSPe6nObBG54zaMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAJia9R70uXdUZtgujUPjLas4+sVajzlBFmqhBqpLAo934vljf9HISsHrPtdBcbS0d0rucqXwabDf0MlR18ksnT/NYpsTwMbMx76CrXi4zYEEW5lISKEO65aIkzVTcqKWSuhjtSnRdB6iOLsFiKmNMWXaIKMR5T0+AbR9wdQgn08W+3EEeHGvafVQfE3STVsSgNb1ft7DvcSUnfPXGU7KzvmTpZa0Hfmc7uY4vpdEEhLAdRhgLReS7USZskov7ooiPSoD+JRFi2gM4klBxTemHdNUa9oFnHMXuYKOkLbkgFvHxyy+QlLq2ELQTga5e7I83ZyOfGctyf8Ul6vGw10vbQ=-----END CERTIFICATE-----"
And The setting "sp-privateKey" is set to "-----BEGIN RSA PRIVATE KEY-----MIIEpAIBAAKCAQEA3cRJZoMfUmIlPL3JElBXuRqE87GAGQgfbHBxgMvWT114o2FIovHtoZ/ETbjj4cFwLpYscpVSc6bSV3arFDhsxiIlfNm6ovruvTsvnDEYCyy/XaU73aqiEaaUfLf5XNFhQGai+7q6mo6/dHYXwcFujslzse+3uE4Qmf+M+4W4X98eK3eapTyXoiSshmOlpbxP18hY5dGr3xTj6GSBamoRlayqV8TwHECz0N4gxlpvQbE72y0cun4ZgrHXS4Th7MVxoNLxb6DYLx6cJO3Udca9yePijnOQLWUhMa9wVOyIWp8hbJd5K59zF1zRbsegboneSzRYnIrfyx8WfS3YCjqRfQIDAQABAoIBAQC5CQAdcqZ9vLpJNilBCJxJLCFmm+HAAREHD8MErg9A5UK1P4S1wJp/0qieGPi68wXBOTgY2xKSwMycgb04/+NyZidVRu388takOW/+KNBg8pMxdZ6/05GqnI0kivSbR3CXpYuz8hekwhpo9+fWmKjApsHL47ItK6WaeKmPbAFsq1YJGzfp/DXg7LIvh9GA3C1LWWGV7SuCGOyX/2Moi8xRa7qBtH4hDo/0NRhTx7zjYjlBgNEr330pJUopc3+AtHE40R+xMr2zkGvq9RsCZxYxD2VWbLwQW0yNjWmQ2OTuMgJJvk2+N73QLHcB+tea82ZTszsNzRS9DLtc6qbsKEPZAoGBAO78U3vEuRyY56f/1hpo0xuCDwOkWGzgBQWkjJl6dlyVz/zKkhXBHpEYImyt8XRN0W3iGZYpZ2hCFJGTcDp32R6UiEyGLz0Uc8R/tva/TiRVW1FdNczzSHcB24b9OMK4vE9JLs8mA8Rp8YBgtLr5DDuMfYt/a/rZJbg/HIfIN98nAoGBAO2OInCX93t2I6zzRPIqKtI6q6FYNp64VIQjvw9Y8l0x3IdJZRP9H5C8ZhCeYPsgEqTXcXa4j5hL4rQzoUtxfxflBUUH60bcnd4LGaTCMYLS14G011E3GZlIP0sJi5OjEhy8fq3zt6jVzS9V/lPHB8i+w1D7CbPrMpW7B3k32vC7AoGAX/HvdkYhZyjAAEOG6m1hK68IZhbp5TP+8CgCxm9S65K9wKh3A8LXibrdvzIKOP4w8WOPkCipOkMlTNibeu24vj01hztr5aK7Y40+oEtnjNCz67N3MQQO+LBHOSeaTRqrh01DPKjvZECAU2D/zfzEe3fIw2Nxr3DUYub7hkvMmosCgYAzxbVVypjiLGYsDDyrdmsstCKxoDMPNmcdAVljc+QmUXaZeXJw/8qAVb78wjeqo1vM1zNgR2rsKyW2VkZB1fN39q7GU6qAIBa7zLmDAduegmr7VrlSduq6UFeS9/qWa4TIBICrUqFlR2tXdKtgANF+e6y/mmaL8qdsoH1JetXZfwKBgQC1vscRpdAXivjOOZAh+mzJWzS4BUl4CTJLYYIuOEXikmN5g0EdV2fhUEdkewmyKnXHsd0x83167bYgpTDNs71jUxDHy5NXlg2qIjLkf09X9wr19gBzDApfWzfh3vUqttyMZuQMLVNepGCWM2vjlY9KGl5OvZqY6d+7yO0mLV9GmQ==-----END RSA PRIVATE KEY-----"
And The setting "security-wantAssertionsSigned" is set to "1"
When I send a GET request to "http://localhost/index.php/login"
Then I should be redirected to "https://localhost:4443/idp/profile/SAML2/Redirect/SSO"
And I send a POST request to "https://localhost:4443/idp/profile/SAML2/Redirect/SSO?execution=e1s1" with the following data
|j_username|j_password|_eventId_proceed|
|student1 |password | |
And The response should be a SAML redirect page that gets submitted
And I should be redirected to "http://localhost/index.php/apps/files/"
And I should be logged-in to Nextcloud as user "student1"
Scenario: Authenticating using Shibboleth with SAML and check if user exists on backend and existing user
Given A local user with uid "student1" exists
And The setting "type" is set to "saml"
And The setting "general-require_provisioned_account" is set to "1"
And The setting "general-uid_mapping" is set to "urn:oid:0.9.2342.19200300.100.1.1"
And The setting "idp-entityId" is set to "https://shibboleth-integration-nextcloud.localdomain/idp/shibboleth"
And The setting "idp-singleSignOnService.url" is set to "https://localhost:4443/idp/profile/SAML2/Redirect/SSO"
And The setting "idp-x509cert" is set to "MIIDnTCCAoWgAwIBAgIUGPx9uPjCu7c172IUgV84Tm94pBcwDQYJKoZIhvcNAQEL BQAwNzE1MDMGA1UEAwwsc2hpYmJvbGV0aC1pbnRlZ3JhdGlvbi1uZXh0Y2xvdWQu bG9jYWxkb21haW4wHhcNMTcwMTA0MTAxMTI3WhcNMzcwMTA0MTAxMTI3WjA3MTUw MwYDVQQDDCxzaGliYm9sZXRoLWludGVncmF0aW9uLW5leHRjbG91ZC5sb2NhbGRv bWFpbjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKH+bPu45tk8/JRk XOxkyfbxocWZlY4mRumEUxscd3fn0oVzOrdWbHH7lCZV4bus4KxvJljc0Nm2K+Zr LoiRUUnf/LQ4LlehWVm5Kbc4kRgOXS0iGZN3SslAWPKyIg0tywg+TLOBPoS6EtST 1WuYg1JPMFxPfeFDWQ0dQYPlXIJWBFh6F2JMTb0FLECqA5l/ryYE13QisX5l+Mqo 6y3Dh7qIgaH0IJNobXoAcEWza7Kb2RnfhZRh9e0qjZIwBqTJUFM/6I86RYXn829s INUvYQQbez6VkGTdUQJ/GuXb/dD5sMQfOyK8hrRY5MozOmK32cz3JaAzSXpiSRS9 NxFwvicCAwEAAaOBoDCBnTAdBgNVHQ4EFgQUKn8+TV0WXSDeavvF0M8mWn1o8ukw fAYDVR0RBHUwc4Isc2hpYmJvbGV0aC1pbnRlZ3JhdGlvbi1uZXh0Y2xvdWQubG9j YWxkb21haW6GQ2h0dHBzOi8vc2hpYmJvbGV0aC1pbnRlZ3JhdGlvbi1uZXh0Y2xv dWQubG9jYWxkb21haW4vaWRwL3NoaWJib2xldGgwDQYJKoZIhvcNAQELBQADggEB ABI6uzoIeLZT9Az2KTlLxIc6jZ4MDmhaVja4ZuBxTXEb7BFLfeASEJmQm1wgIMOn pJId3Kh3njW+3tOBWKm7lj8JxVVpAu4yMFSoQGPaVUgYB1AVm+pmAyPLzfJ/XGhf esCU2F/b0eHWcaIb3x+BZFX089Cd/PBtP84MNXdo+TccibxC8N39sr45qJM/7SC7 TfDYU0L4q2WZHJr4S7+0F+F4KaxLx9NzCvN4h6XaoWofZWir2iHO4NzbrVQGC0ei QybS/neBfni4A2g1lyzCb6xFB58JBvNCn7AAnDJULOE7S5XWUKsDAQVQrxTNkUq7 pnhlCQqZDwUdgmIXd1KB1So="
And The setting "security-authnRequestsSigned" is set to "1"
And The setting "security-wantAssertionsEncrypted" is set to "1"
And The setting "sp-x509cert" is set to "-----BEGIN CERTIFICATE-----MIIC+zCCAeOgAwIBAgIJAIgZuvWDBIrdMA0GCSqGSIb3DQEBBQUAMBQxEjAQBgNVBAMMCWxvY2FsaG9zdDAeFw0xNzAxMDQxMTM5MjFaFw0yNzAxMDIxMTM5MjFaMBQxEjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAN3ESWaDH1JiJTy9yRJQV7kahPOxgBkIH2xwcYDL1k9deKNhSKLx7aGfxE244+HBcC6WLHKVUnOm0ld2qxQ4bMYiJXzZuqL67r07L5wxGAssv12lO92qohGmlHy3+VzRYUBmovu6upqOv3R2F8HBbo7Jc7Hvt7hOEJn/jPuFuF/fHit3mqU8l6IkrIZjpaW8T9fIWOXRq98U4+hkgWpqEZWsqlfE8BxAs9DeIMZab0GxO9stHLp+GYKx10uE4ezFcaDS8W+g2C8enCTt1HXGvcnj4o5zkC1lITGvcFTsiFqfIWyXeSufcxdc0W7HoG6J3ks0WJyK38sfFn0t2Ao6kX0CAwEAAaNQME4wHQYDVR0OBBYEFAoJzX6TVYAwC1GSPe6nObBG54zaMB8GA1UdIwQYMBaAFAoJzX6TVYAwC1GSPe6nObBG54zaMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAJia9R70uXdUZtgujUPjLas4+sVajzlBFmqhBqpLAo934vljf9HISsHrPtdBcbS0d0rucqXwabDf0MlR18ksnT/NYpsTwMbMx76CrXi4zYEEW5lISKEO65aIkzVTcqKWSuhjtSnRdB6iOLsFiKmNMWXaIKMR5T0+AbR9wdQgn08W+3EEeHGvafVQfE3STVsSgNb1ft7DvcSUnfPXGU7KzvmTpZa0Hfmc7uY4vpdEEhLAdRhgLReS7USZskov7ooiPSoD+JRFi2gM4klBxTemHdNUa9oFnHMXuYKOkLbkgFvHxyy+QlLq2ELQTga5e7I83ZyOfGctyf8Ul6vGw10vbQ=-----END CERTIFICATE-----"
And The setting "sp-privateKey" is set to "-----BEGIN RSA PRIVATE KEY-----MIIEpAIBAAKCAQEA3cRJZoMfUmIlPL3JElBXuRqE87GAGQgfbHBxgMvWT114o2FIovHtoZ/ETbjj4cFwLpYscpVSc6bSV3arFDhsxiIlfNm6ovruvTsvnDEYCyy/XaU73aqiEaaUfLf5XNFhQGai+7q6mo6/dHYXwcFujslzse+3uE4Qmf+M+4W4X98eK3eapTyXoiSshmOlpbxP18hY5dGr3xTj6GSBamoRlayqV8TwHECz0N4gxlpvQbE72y0cun4ZgrHXS4Th7MVxoNLxb6DYLx6cJO3Udca9yePijnOQLWUhMa9wVOyIWp8hbJd5K59zF1zRbsegboneSzRYnIrfyx8WfS3YCjqRfQIDAQABAoIBAQC5CQAdcqZ9vLpJNilBCJxJLCFmm+HAAREHD8MErg9A5UK1P4S1wJp/0qieGPi68wXBOTgY2xKSwMycgb04/+NyZidVRu388takOW/+KNBg8pMxdZ6/05GqnI0kivSbR3CXpYuz8hekwhpo9+fWmKjApsHL47ItK6WaeKmPbAFsq1YJGzfp/DXg7LIvh9GA3C1LWWGV7SuCGOyX/2Moi8xRa7qBtH4hDo/0NRhTx7zjYjlBgNEr330pJUopc3+AtHE40R+xMr2zkGvq9RsCZxYxD2VWbLwQW0yNjWmQ2OTuMgJJvk2+N73QLHcB+tea82ZTszsNzRS9DLtc6qbsKEPZAoGBAO78U3vEuRyY56f/1hpo0xuCDwOkWGzgBQWkjJl6dlyVz/zKkhXBHpEYImyt8XRN0W3iGZYpZ2hCFJGTcDp32R6UiEyGLz0Uc8R/tva/TiRVW1FdNczzSHcB24b9OMK4vE9JLs8mA8Rp8YBgtLr5DDuMfYt/a/rZJbg/HIfIN98nAoGBAO2OInCX93t2I6zzRPIqKtI6q6FYNp64VIQjvw9Y8l0x3IdJZRP9H5C8ZhCeYPsgEqTXcXa4j5hL4rQzoUtxfxflBUUH60bcnd4LGaTCMYLS14G011E3GZlIP0sJi5OjEhy8fq3zt6jVzS9V/lPHB8i+w1D7CbPrMpW7B3k32vC7AoGAX/HvdkYhZyjAAEOG6m1hK68IZhbp5TP+8CgCxm9S65K9wKh3A8LXibrdvzIKOP4w8WOPkCipOkMlTNibeu24vj01hztr5aK7Y40+oEtnjNCz67N3MQQO+LBHOSeaTRqrh01DPKjvZECAU2D/zfzEe3fIw2Nxr3DUYub7hkvMmosCgYAzxbVVypjiLGYsDDyrdmsstCKxoDMPNmcdAVljc+QmUXaZeXJw/8qAVb78wjeqo1vM1zNgR2rsKyW2VkZB1fN39q7GU6qAIBa7zLmDAduegmr7VrlSduq6UFeS9/qWa4TIBICrUqFlR2tXdKtgANF+e6y/mmaL8qdsoH1JetXZfwKBgQC1vscRpdAXivjOOZAh+mzJWzS4BUl4CTJLYYIuOEXikmN5g0EdV2fhUEdkewmyKnXHsd0x83167bYgpTDNs71jUxDHy5NXlg2qIjLkf09X9wr19gBzDApfWzfh3vUqttyMZuQMLVNepGCWM2vjlY9KGl5OvZqY6d+7yO0mLV9GmQ==-----END RSA PRIVATE KEY-----"
And The setting "security-wantAssertionsSigned" is set to "1"
When I send a GET request to "http://localhost/index.php/login"
Then I should be redirected to "https://localhost:4443/idp/profile/SAML2/Redirect/SSO"
And I send a POST request to "https://localhost:4443/idp/profile/SAML2/Redirect/SSO?execution=e1s1" with the following data
|j_username|j_password|_eventId_proceed|
|student1 |password | |
And The response should be a SAML redirect page that gets submitted
And I should be redirected to "http://localhost/index.php/apps/files/"
And I should be logged-in to Nextcloud as user "student1"

View file

@ -0,0 +1,232 @@
<?php
/**
* @copyright Copyright (c) 2017 Lukas Reschke <lukas@statuscode.ch>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
use Behat\Behat\Context\Context;
use Behat\Gherkin\Node\TableNode;
class FeatureContext implements Context {
/** @var \GuzzleHttp\Message\Response */
private $response;
/** @var \GuzzleHttp\Client */
private $client;
/** @var array */
private $changedSettings = [];
public function __construct() {
date_default_timezone_set('Europe/Berlin');
}
/** @BeforeScenario */
public function before() {
$jar = new \GuzzleHttp\Cookie\FileCookieJar('/tmp/cookies_' . md5(openssl_random_pseudo_bytes(12)));
$this->client = new \GuzzleHttp\Client([
'cookies' => $jar,
'verify' => false,
'allow_redirects' => [
'referer' => true,
'track_redirects' => true,
],
]);
}
/** @AfterScenario */
public function after() {
$users = [
'student1',
];
foreach($users as $user) {
shell_exec(
sprintf(
'sudo -u apache /opt/rh/rh-php56/root/usr/bin/php %s user:delete %s',
__DIR__ . '/../../../../../../occ',
$user
)
);
}
foreach($this->changedSettings as $setting) {
shell_exec(
sprintf(
'sudo -u apache /opt/rh/rh-php56/root/usr/bin/php %s config:app:delete user_saml %s',
__DIR__ . '/../../../../../../occ',
$setting
)
);
}
$this->changedSettings = [];
}
/**
* @Given The setting :settingName is set to :value
*
* @param string $settingName
* @param string $value
*/
public function theSettingIsSetTo($settingName,
$value) {
$this->changedSettings[] = $settingName;
shell_exec(
sprintf(
'sudo -u apache /opt/rh/rh-php56/root/usr/bin/php %s config:app:set --value="%s" user_saml %s',
__DIR__ . '/../../../../../../occ',
$value,
$settingName
)
);
}
/**
* @When I send a GET request to :url
*/
public function iSendAGetRequestTo($url) {
$this->response = $this->client->request('GET', $url);
}
/**
* @Then I should be redirected to :targetUrl
*
* @param string $targetUrl
* @throws InvalidArgumentException
*/
public function iShouldBeRedirectedTo($targetUrl) {
$redirectHeader = $this->response->getHeader('X-Guzzle-Redirect-History');
$lastUrl = $redirectHeader[count($redirectHeader) - 1];
$url = parse_url($lastUrl);
$targetUrl = parse_url($targetUrl);
$paramsToCheck = [
'scheme',
'host',
'path',
];
// Remove everything after a comma in the URL since cookies are passed there
list($url['path'])=explode(';', $url['path']);
foreach($paramsToCheck as $param) {
if($targetUrl[$param] !== $url[$param]) {
throw new InvalidArgumentException(
sprintf(
'Expected %s for parameter %s, got %s',
$targetUrl[$param],
$param,
$url[$param]
)
);
}
}
}
/**
* @Then I send a POST request to :url with the following data
*
* @param string $url
* @param TableNode $table
*/
public function iSendAPostRequestToWithTheFollowingData($url,
TableNode $table) {
$postParams = $table->getColumnsHash()[0];
$this->response = $this->client->request(
'POST',
$url,
[
'form_params' => $postParams,
]
);
}
/**
* @Then The response should be a SAML redirect page that gets submitted
*/
public function theResponseShouldBeASamlRedirectPageThatGetsSubmitted() {
$responseBody = $this->response->getBody();
$domDocument = new DOMDocument();
$domDocument->loadHTML($responseBody);
$xpath = new DOMXpath($domDocument);
$postData = [];
$inputElements = $xpath->query('//input');
if (is_object($inputElements)) {
/** @var DOMElement $node */
foreach($inputElements as $node) {
$postData[$node->getAttribute('name')] = $node->getAttribute('value');
}
}
$this->response = $this->client->request(
'POST',
'http://localhost/index.php/apps/user_saml/saml/acs',
[
'form_params' => $postData,
]
);
}
/**
* @Then I should be logged-in to Nextcloud as user :userId
* @throws UnexpectedValueException
*/
public function iShouldBeLoggedInToNextcloudAsUser($userId) {
$this->response = $this->client->request(
'GET',
'http://localhost/ocs/v1.php/cloud/user',
[
'headers' => [
'OCS-APIRequest' => 'true',
],
]
);
$xml = simplexml_load_string($this->response->getBody());
$responseArray = json_decode(json_encode((array)$xml), true);
if($responseArray['data']['display-name'] !== $userId) {
throw new UnexpectedValueException(
sprintf(
'Expected %s as value but got %s',
$userId,
$responseArray['data']['display-name']
)
);
}
}
/**
* @Given A local user with uid :uid exists
* @param string $uid
*/
public function aLocalUserWithUidExists($uid) {
shell_exec(
sprintf(
'sudo -u apache OC_PASS=password /opt/rh/rh-php56/root/usr/bin/php %s user:add %s --password-from-env',
__DIR__ . '/../../../../../../occ',
$uid
)
);
}
/**
* @Given The environment variable :key is set to :value
*/
public function theEnvironmentVariableIsSetTo($key, $value) {
file_put_contents(__DIR__ . '/../../../../../../.htaccess', "\nSetEnv $key $value\n", FILE_APPEND);
}
}

7
tests/integration/vendor/autoload.php vendored Executable file
View file

@ -0,0 +1,7 @@
<?php
// autoload.php @generated by Composer
require_once __DIR__ . '/composer/autoload_real.php';
return ComposerAutoloaderInit2b078a63e93bc9e9825cefae96ca1eb3::getLoader();

View file

@ -0,0 +1,818 @@
# Change Log
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).
## [Unreleased]
## [3.3.0] - 2016-12-25
### Added
* [#973](https://github.com/Behat/Behat/pull/974): Added helper containers
* [#973](https://github.com/Behat/Behat/pull/974): Added `SuiteScopedResolverFactory` extension point
## [3.2.3] - 2016-12-25
### Fixed
* [#971](https://github.com/Behat/Behat/pull/971): Added support for suite names with hyphens
## [3.2.2] - 2016-11-05
### Fixed
* [#959](https://github.com/Behat/Behat/issues/959): Fix transformations not sorted properly on different php version
## [3.2.1] - 2016-09-25
### Changed
* [#955](https://github.com/Behat/Behat/pull/955): `--snippets-for` is not required now as interactive mode is the new default
* [#954](https://github.com/Behat/Behat/pull/954): Stop execution on missing steps when running with `--stop-on-failure` and `--strict` options
## [3.2.0] - 2016-09-20
### Added
* [#910](https://github.com/Behat/Behat/pull/910): Return type based transformations
* [#903](https://github.com/Behat/Behat/pull/903): Multiline step definitions support
* [#930](https://github.com/Behat/Behat/pull/930): Whole table transformation
* [#935](https://github.com/Behat/Behat/pull/935): Narrative filters in suites
* [#936](https://github.com/Behat/Behat/pull/936): Debug command
* [#931](https://github.com/Behat/Behat/pull/931): Exception handlers extension point
* [#870](https://github.com/Behat/Behat/pull/870): Added build-related files and folders to .gitattributes
* [#946](https://github.com/Behat/Behat/pull/946): Official full Windows support with CI ([AppVeyor](http://appveyor.com)) on every build
### Changed
* [#922](https://github.com/Behat/Behat/pull/922): Snippets generation revamp
* [#920](https://github.com/Behat/Behat/pull/920): More context for pending/failed steps with progress formatter
* [#905](https://github.com/Behat/Behat/pull/905): Transformations refactoring
* [#864](https://github.com/Behat/Behat/pull/864): Use only one autoloader if possible
* [#920](https://github.com/Behat/Behat/pull/920): Improve "No specifications found" error message
* Refactor changelog to follow [Keep a Changelog](http://keepachangelog.com/)
* Refreshed [CONTRIBUTING.md](CONTRIBUTING.md)
* Refreshed Scrutinizer config
### Fixed
* [#911](https://github.com/Behat/Behat/pull/911): Fix context isolation for Scenario Outlines
* [#860](https://github.com/Behat/Behat/pull/860): Include basepath in `generateKey`
* [#857](https://github.com/Behat/Behat/pull/857): Only cache failed scenario's for rerun
* [#933](https://github.com/Behat/Behat/pull/933): Save failed runs with suite information
* [#833](https://github.com/Behat/Behat/pull/833): Properly handle interupts on PHP7
* [#904](https://github.com/Behat/Behat/pull/904): Provide clearer exception message when long token names used
* [#941](https://github.com/Behat/Behat/pull/941): Transformation should be allowed if printable chars are used
### Deprecated
* [#922](https://github.com/Behat/Behat/pull/922): `*SnippetAcceptingContext` interfaces
* [#905](https://github.com/Behat/Behat/pull/905): `RuntimeTransformation`
* [#905](https://github.com/Behat/Behat/pull/905): `Transformation::getPattern`
* [#920](https://github.com/Behat/Behat/pull/920): `StepStat`
### Removed
* Remove behat.bat (by Konstantin Kudryashov)
## [3.1.0] - 2016-03-28
### Changed
* Add support for Symfony 3 (thanks @benji07)
* Add ability to specify execution order of suite (thanks @ciaranmcnulty)
* Add translated keywords in definition printer (thanks @WouterJ)
* Add 'rowtable' transformations (thanks @PurpleBooth)
* Add 'narrative' filters (thanks @WouterJ)
* Add JUnit formatter (thanks @WouterJ and @james75)
* Add Japanese translation (thanks @SNakano)
* Add romanian translation for formatters (thanks @Chriton)
* Add table row transformations (thanks @ciaranmcnulty)
* Add support for negative numbers without surrounding quotes (thanks @ryancookdev)
* Handle case when non-existent config file is used (thanks @watermanio)
* Handle non-default `error_reporting()`
* Handle PHP7 errors implementing `Throwable`
* Fix autoloading from the global installation (thanks @sroze)
* Fix scenario scope naming (thanks @Taluu)
* Fix output buffering errors (thanks @tscheepers)
* Fix xdebug maximum nesting level errors (thanks @WorkingDevel)
* Fix weird edge case in GroupedSpecificationIterator
* Allow --verbose flag at CLI (thanks @pfrenssen)
* Allow hyphens in suite names (thanks @WouterJ)
* Allow suite settings with null values to exist (thanks @docteurklein)
* Improve "can not generate snippets" message
* Improve performance of Turnip parsing (thanks @Sam-Burns)
* Improve the snippet generation by auto-importing needed classes (thanks @stof)
## [3.0.15] - 2015-02-22
### Changed
* Fix broken null-transformations (Issue #669)
* Improve exception messages (thanks @dantleech)
## [3.0.14] - 2014-09-23
### Changed
* Improve generated context class
## [3.0.13] - 2014-08-28
### Changed
* Add support for typehinted parameters
* Allow any whitespace characters at the end of context class
* Fix scenario with decimal number following string in Turnip pattern
* Fix scenario with empty string in step with Turnip pattern
* Fix scenario where step has slashes in Turnip pattern
## [3.0.12] - 2014-07-17
### Changed
* Fix remaining issues with the definition arguments parsing
* Introduce `Testwork\Argument` component
## [3.0.11] - 2014-07-09
### Changed
* Fix argument resolution for functions with default values (thanks @alesblaznik)
* Fix step colouring of internationalised definitions
* Refactor `ContextFactory` and `RepositorySearchEngine` arguments resolution into the new
Testwork component - `ArgumentResolver`
## [3.0.10] - 2014-06-29
### Changed
* Fix argument resolution when named arguments used and method has defaults (thanks @WouterJ)
* Fix support for decimal numbers in turnip placeholders
## [3.0.9] - 2014-06-20
### Changed
* Fix definition translations reading bug with multi-suite configurations (thanks @WouterJ for reporting)
* Fix pretty printer bug with failing background and 2 scenarios (thanks @andytson for reporting)
* Fix memory footprint calculation (thanks @dready for reporting)
## [3.0.8] - 2014-06-06
### Changed
* Profile level Gherkin filters are now overridable by CLI filter options
* Rerun cache path is now configurable
* Fix turnip-based step definitions starting from token
* Fix token-based transformations interfering with regex-based ones
* Rerun cache dump have been optimised
## [3.0.7] - 2014-05-27
### Changed
* Properly generate keywords in snippets for non-english and `And`, `But` steps (thanks @kibao)
* Fix regex check bug with transformations that return objects (thanks @vaidasm)
* Return ability to use custom formatters by specifiying their class names
## [3.0.6] - 2014-05-06
### Changed
* Fix a small extension registration shortcut issue introduced in previous release (thanks @FrenkyNet)
## [3.0.5] - 2014-05-06
### Changed
* Fix a suite initialization bug when suite contexts have arguments
* Fix wrong handling of an empty `behat.yml`
* Explicitly fail when provided context argument is not supported by constructor
* Fix extension registration shortcut for 3rd-part plugins
## [3.0.4] - 2014-04-29
### Changed
* Make sure that `Before*Tested` is always executed before `Before*` hooks
* Introduce additional `After*Setup` and `Before*Teardown` events
* Improved the error reporting for invalid regexes in step definitions (thanks @stof)
## [3.0.3] - 2014-04-27
### Changed
* Support definition transformations without capture groups
* Override gherkin filters in custom profiles instead of merging them
* Refactored the handling of colors to set them earlier
([#513](https://github.com/Behat/Behat/pull/513) thanks to @stof)
## [3.0.2] - 2014-04-26
### Changed
* Fix warning on empty scenarios
## [3.0.1] - 2014-04-26
### Changed
* Make sure that `AfterStep` hook is running even if step is failed
([504](https://github.com/Behat/Behat/issues/504))
* Optimised the way service wrappers are registered (thanks @stof)
## [3.0.0] - 2014-04-20
### Changed
* Brand new highly extendable and clear architecture
* Support for multiple suites per profile
* Support for multiple contexts per suite
* Support for multiple feature paths per suite
* Support for filtered suites
* Support for unique context constructor parameters
* Hooks are first class citizens and thus have their own error and output buffering
* Turnip syntax in definitions
* Reworked formatters with improved error and output buffering
* Rerun does not require precache run
* New gherkin role filter
* Improved error handling with 3 levels of error reporting (-v, -vv, -vvv)
* Dropped subcontexts
* Dropped chained steps
* Dropped closured definitions
## 3.0.0rc3 - 2014-03-16
### Changed
* Multiline step description support ([082da36b7db2525700287616babe982e485330d1](https://github.com/Behat/Behat/commit/082da36b7db2525700287616babe982e485330d1))
* Added ability to choose all 3 verbosity levels and moved stack traces to the 2nd one ([d550f72d6aa49f0f87a6ce0e50721356a5d04c45](https://github.com/Behat/Behat/commit/d550f72d6aa49f0f87a6ce0e50721356a5d04c45))
* Renamed Subject to Specification ([#447](https://github.com/Behat/Behat/pull/447))
* Refactored ContextSnippetGenerator ([#445](https://github.com/Behat/Behat/pull/445))
* Refactored context arguments handling ([#446](https://github.com/Behat/Behat/pull/446))
* Refactored testers to use composition over inheritance and added setUp/tearDown phase to them ([#457](https://github.com/Behat/Behat/pull/457))
* Refactored output formatters to be chain of event listeners
* Refactored hooks to use [scopes](https://github.com/Behat/Behat/tree/3.0/src/Behat/Behat/Hook/Scope) instead of events
* Fixed the GroupedSubjectIterator when dealing with an empty iterator ([2c1312780d610f01116ac42fb958c0c09a64c041](https://github.com/Behat/Behat/commit/2c1312780d610f01116ac42fb958c0c09a64c041))
* Forced the paths.base to use a real path all the time ([b## [4477d7cf3f9550874c609d4edc5a4f55390672c](https://github.com/Behat/Behat/commit/b4477d7cf3f9550874c609d4edc5a4f55390672c))
3.0.0rc2] - 2014-01-10
### Changed
* Fixed progress formatter hooks support
* Reintroduced suite hooks (with an additional functionality of name filtering)
* Behat tells about steps that it couldn't generate snippets for
* Memory consumption optimizations
* Fixed contexts inheritance
* New formatter translations
* Added constructor arguments and class resolving extension points to context creation routine
* Simplified and cleaned `Context` package of the Behat
* Minor public API changes across the board (simplification)
* Optimized subject finding routine and cleaned extension points (`SubjectLocator`)
* Both `ExampleTested` and `ScenarioTested` now use same method name - `getScenario()`
* Added exception accessors to `StepTestResult`
* Renamed `ExerciseTester` to `Exercise`
* Added `HookableEvent` to Testwork, which extends `LifecycleEvent`
* Made `priority` attribute of a tag optional
* Changed all occurrences of `classname` to `class` across public API
* Renamed `GherkinSuite` to `GenericSuite` and moved it into the Testwork
* Added `initialize` call to extension lifecycle and Extension interface
* Renamed some extensions config keys to be more intuitive
## 3.0.0rc1 - 2014-01-01
### Changed
* New layered and highly extendable architecture
* Standard output buffering of definitions and hooks
* Hooks as first class citizens
* New pretty and progress formatters
* Huge speed and memory footprint improvements
* Moved 40% of non-Behat related codebase into a shared foundation called Testwork
## 3.0.0beta8 - 2013-10-01
### Changed
* Add `*SnippetsFriendlyInterface`(s) that are now required to generate snippets
* Add support for turnip-style definitions
* Use turnip-style definitions by default from `--init`
* Rename `SuitesLoader` to `SuitesRegistry` to clarify purpose
* Extract snippet generators into extendable component
* Extract context generators into extendable component
## 3.0.0beta7 - 2013-09-29
### Changed
* Multivalue options are now array options (format, output, name and tags)
* Added back junit formatter (should support all junit formats from 4 to 7)
* Added back html formatter
* Small optimizations and refactorings
* Proper handling of hook failures
## 3.0.0beta6 - 2013-09-25
### Changed
* Skip step execution and `AfterStep` hook if its `BeforeStep` hook failed
* Fix failure-initiated skips of hooks in Scenario and Example testers
* Refactor Suite routines
* Cleanup Context Pools
* Enhance `--definitions` option with suites output and regex search
* Add `toString()` methods to `DefinitionInterface` and `TransformationInterface`
* Add `SnippetlessContextInterface` to `Snippet` namespace - to prevent snippet generation for
custom contexts
## 3.0.0beta5 - 2013-09-15
### Changed
* Switch to Gherkin 3.0 parser
* Complete rewrite of pretty formatter (much better outline handling)
* Automatically add `use` for `PendingException` to contexts during `--append-snippets`
* Lots of optimizations
## 3.0.0beta4 - 2013-08-17
### Changed
* Cleanup suite configuration sub-system
* New ability to turn off specific suites through `behat.yml`
* Support for danish language
## 3.0.0beta3 - 2013-08-13
### Changed
* Refactor extension sub-system. Update `ExtensionInterface`
* Avoid trying to create folders for non-fs suites
## 3.0.0beta2 - 2013-08-13
### Changed
* Remove support for Symfony 2.0 components
## 3.0.0beta1 - 2013-08-13
### Changed
* New suite-centric architecture
* New context pools sub-system with multi-context support
* New dynamic event-driven testing core
* Refactored console processors sub-system
* Refactored formatters management sub-system
* 8 new process extension points and 36 generic execution extension points
* Gherkin caching is enabled by default
* Rerun is enabled by default (use `--rerun` to rerun failed scenarios)
* New Gherkin Role filter
* Subcontexts removed in favor of context pools
* Chained steps extracted into [separate extension](https://github.com/Behat/ChainedStepsExtension)
* Closured step definitions removed
## 2.5.0 - 2013-08-11
### Changed
* First Behat LTS release
* Update Junit formatter to reflect latest junit format (thanks @alistairstead)
* Fix some container options
## 2.4.6 - 2013-06-06
### Changed
* New --stop-on-failure option
* Support JSON in environment variables
* Update Gherkin
* Support Symfony 2.3
* Out-of-the-box support for PHPUnit assertions pretty output
## 2.4.5 - 2013-01-27
### Changed
* Added wrapping of lines in progress formatter
* Added `--append-to` option to be able to add snippets to custom class
* Both `ScenarioEvent` and `OutlineExampleEvent` now extend same `BaseScenarioEvent` class
* Highly improved ability to create simple custom extensions
* Always hide stack traces for `PendingException`
* Ensured compatibility with all major symfony versions
* Fixed configs import directive and loading precedence
* Fixed path to vendor dir (solves problem of custom vendor dirs)
## 2.4.4 - 2012-09-12
### Changed
* Fixed `RuntimeException` namespacing error
* Added `FormatterManager::disableFormatter(s)` method
* Updated Gherkin parser and fixed couple of helper bugs
## 2.4.3 - 2012-07-28
### Changed
* Fixed broken `output_path` setting ([issue #169](https://github.com/Behat/Behat/issues/169))
* Added shellbang to phar executable ([issue #167](https://github.com/Behat/Behat/issues/167))
* Added feature title to progress exceptions ([issue #166](https://github.com/Behat/Behat/issues/166))
* Tuned failed formatter to print only failed examples in outline ([issue #154](https://github.com/Behat/Behat/issues/154))
* Small bugfixes
## 2.4.2 - 2012-06-26
### Changed
* Fixed broken autoloading with Composer installation
## 2.4.1 - 2012-06-26
### Changed
* Force custom context class usage if user changed it from `FeatureContext`
* Clarified `Context class not found` exception
* Use CWD for CLI options, basepath (config path) for everything else
* Pass `behat.extension.classes` container param to extensions during their load
* Tuned `event_subscriber` priorities
* Use `require_once` instead of `require` in closured loaders
* Fixed transformers bug with falsy transformations (that return **falsy** values)
* Fixed custom formatters definition bug
* Fixed formatter manager exception bug
* Fixed czech translation
* Fixed CS to be PSR2 compliant
## 2.4.0 - 2012-05-15
### Changed
* New extension system based on Symfony2 DIC component
* Refactored paths reading system (now relative paths are fully supported)
* Support latest Composer changes
* Removed static constraint for transformations
* Updated to latest Gherkin with immutable AST
* Fixed couple of definition snippet generator bugs
* Option for HTML formatter to provide step definition links
* Added fallback locale (in case if provided lang is unsupported yet)
* Print step snippets in HTML formatter only if they're enabled
* Escape placeholder brackets in HTML formatter
* Use different names for examples in JUnit formatter
* Major core cleanup
## 2.3.5 - 2012-03-30
### Changed
* Fixed formatter language configuration and locale guesser
## 2.3.4 - 2012-03-28
### Changed
* Added `StepEvent::getLogicalParent()`. Fixed issue ### 115
2.3.3 - 2012-03-09
### Changed
* Implemented Gherkin caching support ([--cache](https://github.com/Behat/Behat/commit/753c4f6e392a873a640543306191d92e6dc91099))
* Line ranges filtering support (`behat features/some.feature:12-19`. Thanks @headrevision)
* `behat.yml.dist` configs support out of the box
* Minor bug fixes
* Updated Gherkin
## 2.3.2 - 2012-01-29
### Changed
* Fixed bug in `ErrorException`, that caused wrong exceptions on warnings and notices
## 2.3.1 - 2012-01-26
### Changed
* Updated error handler to avoid suppressed exceptions
* Autoload bootstrap scripts in their name order
* Updated Gherkin dependency to v## 2.0.1
2.3.0 - 2012-01-19
### Changed
* Switch to the Behat\Gherkin 2.0 usage
* Migration to the single-file translation
* Support for callables inside steps chains
* Support for `*.yml` and `*.php` as definition translations
* Added opposite options to option switchers (`--[no-]colors`, `--[no-]multiline`, etc.)
* Redesigned `--story-syntax`
* Refactored Runner
* Performance improvements
* Bugfixes
## 2.2.7 - 2012-01-13
### Changed
* Added ability to search translated definitions with `--definitions`
* Fixed custom formatters use bug
## 2.2.6 - 2012-01-09
### Changed
* Fixed pretty and html formatters printing of undefined steps in outlines
## 2.2.5 - 2012-01-07
### Changed
* `BEHAT_PARAMS` env variable support (083092e)
* HTML formatter print styles optimization (@davedevelopment)
## 2.2.4 - 2012-01-04
### Changed
* Prevent method name duplication with definition snippets
## 2.2.3 - 2012-01-04
### Changed
* Fixed couple of `--append-snippets` bugs
## 2.2.2 - 2011-12-21
### Changed
* Fixed Composer deps
## 2.2.1 - 2011-12-21
### Changed
* Fixed Composer package bin
## 2.2.0 - 2011-12-14
### Changed
* Multiple formats and outputs support
* New `snippets` formatter
* New `failed` formatter
* Updated output of `-d` option
* Search abilities added to `-d` option
* New `--dry-run` option
* New `--append-snippets` option
* Rerun functionality refactored to use `failed` formatter internally
* Overall code refactoring and cleaning
* Polish translation added (Joseph Bielawski)
* Spanish translation updated (Andrés Botero)
* Locale autodetect
## 2.1.3 - 2011-11-04
### Changed
* Substep translations support
* Correctly print undefined substeps in pretty printer
* @Transform callback now gets all provided matches
* Always set proper encoding (UTF## 8)
2.1.2 - 2011-10-12
### Changed
* Fixed filtered feature hooks
* Fixed JUnit formatter time output in some locales
## 2.1.1 - 2011-10-09
### Changed
* Fixed multiline titles printing bug
* Fixed outline parameter inside step argument printing bug
## 2.1.0 - 2011-09-12
### Changed
* Totally revamped HTML formatter template
* Added transliteration support to definition snippets (for most langs)
* Written missed features and fixed some bugs
* Stabilization fixes for 3 major OS: MacOS/Ubuntu/Windows
## 2.0.5 - 2011-08-07
### Changed
* Cleaned ContextDispatcher extension points
* Cleaned context-parameters passing behavior
## 2.0.4 - 2011-08-02
### Changed
* Subcontexts aliasing and retrieving
* Multiple steps chaining
* `--snippets-paths` option to show steps alongside the snippets
* getContextParameters() method in SuiteEvent and FeatureEvent
* Updated to Symfony2 stable components
* Spanish translation
* Dutch translation
## 2.0.3 - 2011-07-20
### Changed
* Fixed JUnit formatter CDATA output
## 2.0.2 - 2011-07-17
### Changed
* Added extra checks to context instance mapper
* Fixed i18n support in definitions printer
* Refactored Gherkin tags inheritance
## 2.0.1 - 2011-07-12
### Changed
* Exception prefix added to statuses. Now you should throw `PendingException` instead of just
`Pending`
## 2.0.0 - 2011-07-12
### Changed
* Brand new Context-oriented architecture
* Refactored --definitions (--steps) to print more useful info
* Rafactored --story-syntax (--usage) to print more useful info
* Refactored Command to use separate processors
* Added --no-paths option
* Added --no-snippets option
* Added --expand option to expand outlines
* phar package
* Faster autoloader
* Steps chaining added
* Added BEHAT_ERROR_REPORTING constant to change error_repoting level
* Fixed some Gherkin bugs
* Fixed lots of bugs in Behat itself
## 1.1.9 - 2011-06-17
### Changed
* Updated to the latest Symfony components
## 1.1.8 - 2011-06-09
### Changed
* Fixed empty match printing in Pretty and HTML formatters
* Updated to latest Symfony components
## 1.1.7 - 2011-06-03
### Changed
* Fixed steps colorization bug in outline
* Additional checks in config import routine
## 1.1.6 - 2011-05-27
### Changed
* Updated Symfony vendors
* Refactored console formatters
## 1.1.5 - 2011-05-17
### Changed
* Fixed CWD path finding
* Fixed HTML formatter (thanks @glenjamin)
## 1.1.4 - 2011-05-03
### Changed
* Fixed `--out` option usage critical bug
* Added ability to specify `output_path` from config file
## 1.1.3 - 2011-04-28
### Changed
* JUnit formatter fix
* Formatters basePath fix. Now formatters uses CWD as path trimmer
* Relative paths locator bug fix
* Show table argument header in HTML formatter
## 1.1.2 - 2011-04-27
### Changed
* Fixed custom features path locator bug(issue ### 020)
1.1.1 - 2011-04-21
### Changed
* Fixed paths finding routines
* Totally refactored BehatCommand
* Added rerun functionality (`--rerun`)
* Ability to remove previously specified paths in `behat.yml`
* Bugfixes and little tweaks
## 1.1.0 - 2011-04-04
### Changed
* New configuration system with profiles and imports support
* New event system
* Environment parameters support
* Named regex arguments support
* Japanese translation for formatters
* JUnit formatter bugfixes
* HTML and Pretty formatters multiple arguments print bugfix
* Step snippets (proposals) bugfixes
* Updated vendor libraries
## 1.0.0 - 2011-03-08
### Changed
* Changed XSD
* Updated vendors
## 1.0.0RC6 - 2011-03-03
### Changed
* Cleaned command options
* Added --init option
* Multiple paths support in behat.yml
* Application options refactoring
## 1.0.0RC5 - 2011-02-25
### Changed
* Windows support
* Bundled features hooks optimizations
## 1.0.0RC4 - 2011-02-23
### Changed
* Pretty formatter tag printing fix
* Custom formatter specification fix in `behat.yml`
* Symfony components updated
* Extension configuration manager (Symfony\Component\Config component)
* Cleaning of `behat.yml` configurator (thanks to Symfony\Component\Config)
* Additional formatter parameters support in `behat.yml`
## 1.0.0RC3 - 2011-02-18
### Changed
* Event dispatcher binding optimizations
* Command API optimizations for easier overloading
* Formatter path trimming bugfix
* BehatExtension config merging support
## 1.0.0RC2 - 2011-02-15
### Changed
* Step printing option bugfix
## 1.0.0RC1 - 2011-02-15
### Changed
* Gherkin DSL parser is standalone project
* Own Behat namespace for both Behat & Gherkin
* Fully rewritten formatters (much cleaner & beautifull API)
* Big refactoring of whole Behat code (clean code DRYing)
* Config file is now handled by standart-driven DIC extension (cleaner `behat.yml`)
* API documentation retouched
* New `--strict` option
* New `--no-multiline` option
* Feature examples in your language with `--usage`
* Available definitions listing with `--steps`
* Definition i18n
* Command refactoring (much cleaner API & actions)
* Event system refactoring
* 42 new languages with new Gherkin DSL parser
## 0.3.6 - 2010-12-07
### Changed
* [Behat,Gherkin] Fixed French support includes (fr)
## 0.3.6 - 2010-12-06
### Changed
* [Behat] Updated Symfony2 Components to latest PR4
* [Gherkin] Added French support (fr)
* [Gherkin] Added German support (de)
* [Behat] Small bugfixes
## 0.3.5 - 2010-11-19
### Changed
* [Behat] Refactored EnvironmentBuilder to allow Environment service definition overload
## 0.3.4 - 2010-11-18
### Changed
* [Behat] Introduced environment builder
* [Gherkin,Behat] id locale support
## 0.3.3 - 2010-11-07
### Changed
* [Gherkin] Added ability to create Table & PyString nodes with hands (in your step to step calls for example)
* [Gherkin] Added getRowsHash() method to TableNode, so now you can "rotate" given tables
* [Gherkin] You now can add comments before language specification in your feature files
## 0.3.2 - 2010-11-06
### Changed
* [Gherkin] Added ability to specify extended langs (en-US)
* [Behat,Gherkin] Added pt-BR translation
## 0.3.1 - 2010-11-02
### Changed
* [Behat] JUnit formatter
* [Behat] Pretty & HTML formatter background hooks fix
* [Behat] Other small fixes
## 0.3.0 - 2010-11-02
### Changed
* [Behat] Refactored tags filter
* [Behat] Added name filter
* [Behat] Refactored hooks
* [Behat] Added tagged/named hooks
* [Behat] Customizable HTML formatter with w3c valid default markup
* [Behat] Ability to specify out path for formatters
* [Behat] Bunch of new options
* [Behat] DIC optimisations
## 0.2.5 - 2010-10-22
### Changed
* [Behat] Format manager introduced
* [Behat] Formatters refactoring
* [Behat] Optmized container parameters to support EverzetBehatBundle
* [Behat] --no-color => --no-colors
## 0.2.4 - 2010-10-19
### Changed
* [Behat] Autoguess of colors support
* [Behat] Formatter setup bugfix (properl casing)
## 0.2.3 - 2010-10-19
### Changed
* [Behat] Filters optimisations
* [Behat] Changed Core Loaders with topic-specific (`StepDefinition\Loader\PHPLoader`,
`Features\Loader\GherkinLoader`)
* [Behat] Simplified TestCommand in prepare of Symfony2 BehatBundle
* [Behat] Configuration file/path setting update (you can now create `behat.yml` inside `./config/behat.yml` & Behat
will load it
* [Behat] Updated Redundant & Ambiguous exceptions behavior
## 0.2.2 - 2010-10-10
### Changed
* [Behat] Configuration file/path setting update
## 0.2.1 - 2010-10-10
### Changed
* [PEAR] Fix path to phpbin on installation
## 0.2.0 - 2010-10-08
### Changed
* [Behat] Brand new stateless testers, based on Visitor pattern
* [Behat] Refactored event listeners & event names
* [Behat] Refactored formatters to confirm with new stateless testers (statuses now sent as event parameters)
* [Behat] Refactored ConsoleFormatter (and removed base formatter)
* [Behat] Removed custom I18n classes & refactored Translator routines in flavor of Symfony\Component\Translation
* [Behat] Added missed translation strings into XLIFF files
* [Behat] Optimised multiline arguments (Node instances are sent to definitions instead of their plain representations)
* [Behat] Support for Scenario Outline tokens replace in multiline arguments (tables & pystrings)
* [Behat] Step arguments transformations (including table transformations)
* [Behat] Colorize inline step arguments
* [Behat] Optimized exit statuses of CLI
* [Behat] Added ability to turn-off colors
* [Behat] Added ability to translate formatters output with `--i18n` option
* [Behat] Bunch of new core feature tests
* [Gherkin] Parser now uses Symfony Dependency Injection to
* [Gherkin] Refactored parser to be like AST (Nodes that supports Visitor pattern)
* [Gherkin] Comments support
* [Gherkin] Fixed PHPUnit warnings
* [Behat,Gherkin] PEAR release script to support http://pear.everzet.com release model
* [Behat,Gherkin] DIC naming refactoring
* [Behat,Gherkin] Autoloader refactoring
* [Behat,Gherkin] Removed Zend & Goutte depencies
## 0.1.5 - 2010-09-25
### Changed
* Added ability to call other steps inside step definition
* Added profiles
* Refactored container creation routine
* Single quotes support in step definitions
* Added tests for hooks, profiles, inline steps
## 0.1.4 - 2010-09-16
### Changed
* Refactored code
* Removed logic from object constructors
* Added Loader & Filter interfaces
## 0.1.3 - 2010-09-14
### Changed
* Ability to specify arrays of paths/files for loaders
* Event hooks and support for `support/hooks.php`
* Formatters listens events with smallest priority
* Don't try to load steps if `steps` folder doesn't exists
* Bugfixes/refactoring
## 0.1.2 - 2010-09-10
### Changed
* Added ability to read from `behat.yml` and `behat.xml`
* Moved tags filter to separate object
* Refactored injection controller
* Optimized event names in event dispatcher
* Other small fixes/refactorings
## 0.1.1 - 2010-09-09
### Changed
* Added `--tags` option
* Changed environment (world) routines
* Added lots of core tests (writed in Behat itself)
## 0.1.0 - 2010-09-08
### Changed
* Initial release
[Unreleased]: https://github.com/Behat/Behat/compare/v3.3.0...HEAD
[3.3.0]: https://github.com/Behat/Behat/compare/v3.2.3...v3.3.0
[3.2.3]: https://github.com/Behat/Behat/compare/v3.2.2...v3.2.3
[3.2.2]: https://github.com/Behat/Behat/compare/v3.2.1...v3.2.2
[3.2.1]: https://github.com/Behat/Behat/compare/v3.2.0...v3.2.1
[3.2.0]: https://github.com/Behat/Behat/compare/v3.1.0...v3.2.0
[3.1.0]: https://github.com/Behat/Behat/compare/v3.0.15...v3.1.0
[3.0.15]: https://github.com/Behat/Behat/compare/v3.0.14...v3.0.15
[3.0.14]: https://github.com/Behat/Behat/compare/v3.0.13...v3.0.14
[3.0.13]: https://github.com/Behat/Behat/compare/v3.0.12...v3.0.13
[3.0.12]: https://github.com/Behat/Behat/compare/v3.0.11...v3.0.12
[3.0.11]: https://github.com/Behat/Behat/compare/v3.0.10...v3.0.11
[3.0.10]: https://github.com/Behat/Behat/compare/v3.0.9...v3.0.10
[3.0.9]: https://github.com/Behat/Behat/compare/v3.0.8...v3.0.9
[3.0.8]: https://github.com/Behat/Behat/compare/v3.0.7...v3.0.8
[3.0.7]: https://github.com/Behat/Behat/compare/v3.0.6...v3.0.7
[3.0.6]: https://github.com/Behat/Behat/compare/v3.0.5...v3.0.6
[3.0.5]: https://github.com/Behat/Behat/compare/v3.0.4...v3.0.5
[3.0.4]: https://github.com/Behat/Behat/compare/v3.0.3...v3.0.4
[3.0.3]: https://github.com/Behat/Behat/compare/v3.0.2...v3.0.3
[3.0.2]: https://github.com/Behat/Behat/compare/v3.0.1...v3.0.2
[3.0.1]: https://github.com/Behat/Behat/compare/v3.0.0...v3.0.1
[3.0.0]: https://github.com/Behat/Behat/compare/v2.5.5...v3.0.0

22
tests/integration/vendor/behat/behat/LICENSE vendored Executable file
View file

@ -0,0 +1,22 @@
Copyright (c) 2016 Konstantin Kudryashov <ever.zet@gmail.com>
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.

View file

@ -0,0 +1,74 @@
![Behat](https://dl.dropboxusercontent.com/u/282797/behat/behat.png)
Behat is a BDD framework for PHP to help you test business expectations.
[![Gitter chat](https://badges.gitter.im/Behat/Behat.svg)](https://gitter.im/Behat/Behat)
[![License](https://poser.pugx.org/behat/behat/license.svg)](https://packagist.org/packages/behat/behat)
[![Unix Status](https://travis-ci.org/Behat/Behat.svg?branch=master)](https://travis-ci.org/Behat/Behat)
[![Windows status](https://ci.appveyor.com/api/projects/status/9uc5sellmvbv02ei/branch/master?svg=true)](https://ci.appveyor.com/project/everzet/behat/branch/master)
[![HHVM Status](http://hhvm.h4cc.de/badge/behat/behat.svg?branch=master)](http://hhvm.h4cc.de/package/behat/behat)
[![Scrutinizer Quality Score](https://scrutinizer-ci.com/g/Behat/Behat/badges/quality-score.png?s=ad84e95fc2405712f88a96d89b4f31dfe5c80fae)](https://scrutinizer-ci.com/g/Behat/Behat/)
Installing Behat
----------------
The easiest way to install Behat is by using [Composer](https://getcomposer.org):
```bash
$> curl -sS https://getcomposer.org/installer | php
$> php composer.phar require behat/behat
```
After that you'll be able to run Behat via:
```bash
$> vendor/bin/behat
```
Installing Development Version
------------------------------
Clone the repository and install dependencies via [Composer](https://getcomposer.org):
```bash
$> curl -sS https://getcomposer.org/installer | php
$> php composer.phar install
```
After that you will be able to run development version of Behat via:
```bash
$> bin/behat
```
Contributing
------------
Before contributing to Behat, please take a look at the [CONTRIBUTING.md](CONTRIBUTING.md) document.
Versioning
----------
Starting from `v3.0.0`, Behat is following [Semantic Versioning v2.0.0](http://semver.org/spec/v2.0.0.html).
This basically means that if all you do is implement interfaces (like [this one](https://github.com/Behat/Behat/blob/v3.1.0/src/Behat/Behat/Context/ContextClass/ClassResolver.php#L15-L22))
and use service constants (like [this one](https://github.com/Behat/Behat/blob/v3.1.0/src/Behat/Behat/Context/ServiceContainer/ContextExtension.php#L46)),
you would not have any backwards compatibility issues with Behat up until `v4.0.0` (or later major)
is released. Exception could be an extremely rare case where BC break is introduced as a measure
to fix a serious issue.
You can read detailed guidance on what BC means in [Symfony2 BC guide](http://symfony.com/doc/current/contributing/code/bc.html).
Useful Links
------------
- The main website is at [http://behat.org](http://behat.org)
- The documentation is at [http://docs.behat.org/en/latest/](http://docs.behat.org/en/latest/)
- Official Google Group is at [http://groups.google.com/group/behat](http://groups.google.com/group/behat)
- IRC channel on [#freenode](http://freenode.net/) is `#behat`
- [Note on Patches/Pull Requests](CONTRIBUTING.md)
Contributors
------------
- Konstantin Kudryashov [everzet](http://github.com/everzet) [lead developer]
- Other [awesome developers](https://github.com/Behat/Behat/graphs/contributors)

View file

@ -0,0 +1,34 @@
#!/usr/bin/env php
<?php
/*
* This file is part of the Behat.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
define('BEHAT_BIN_PATH', __FILE__);
if (is_file($autoload = getcwd() . '/vendor/autoload.php')) {
require $autoload;
}
if (!class_exists('Behat\Behat\ApplicationFactory', true)) {
if (is_file($autoload = __DIR__ . '/../vendor/autoload.php')) {
require($autoload);
} elseif (is_file($autoload = __DIR__ . '/../../../autoload.php')) {
require($autoload);
} else {
fwrite(STDERR,
'You must set up the project dependencies, run the following commands:'.PHP_EOL.
'curl -s http://getcomposer.org/installer | php'.PHP_EOL.
'php composer.phar install'.PHP_EOL
);
exit(1);
}
}
$factory = new \Behat\Behat\ApplicationFactory();
$factory->createApplication()->run();

View file

@ -0,0 +1,57 @@
{
"name": "behat/behat",
"description": "Scenario-oriented BDD framework for PHP 5.3",
"keywords": ["BDD", "ScenarioBDD", "StoryBDD", "Examples", "Scrum", "Agile", "User story", "Symfony", "business", "development", "testing", "documentation"],
"homepage": "http://behat.org/",
"type": "library",
"license": "MIT",
"authors": [
{
"name": "Konstantin Kudryashov",
"email": "ever.zet@gmail.com",
"homepage": "http://everzet.com"
}
],
"require": {
"php": ">=5.3.3",
"ext-mbstring": "*",
"behat/gherkin": "^4.4.4",
"behat/transliterator": "~1.0",
"symfony/console": "~2.5||~3.0",
"symfony/config": "~2.3||~3.0",
"symfony/dependency-injection": "~2.1||~3.0",
"symfony/event-dispatcher": "~2.1||~3.0",
"symfony/translation": "~2.3||~3.0",
"symfony/yaml": "~2.1||~3.0",
"symfony/class-loader": "~2.1||~3.0",
"container-interop/container-interop": "^1.1"
},
"require-dev": {
"symfony/process": "~2.5|~3.0",
"phpunit/phpunit": "~4.5",
"herrera-io/box": "~1.6.1"
},
"suggest": {
"behat/symfony2-extension": "for integration with Symfony2 web framework",
"behat/yii-extension": "for integration with Yii web framework",
"behat/mink-extension": "for integration with Mink testing framework"
},
"autoload": {
"psr-0": {
"Behat\\Behat": "src/",
"Behat\\Testwork": "src/"
}
},
"extra": {
"branch-alias": {
"dev-master": "3.2.x-dev"
}
},
"bin": ["bin/behat"]
}

217
tests/integration/vendor/behat/behat/i18n.php vendored Executable file
View file

@ -0,0 +1,217 @@
<?php return array(
'en' => array(
'snippet_context_choice' => '<snippet_undefined><snippet_keyword>%1%</snippet_keyword> suite has undefined steps. Please choose the context to generate snippets:</snippet_undefined>',
'snippet_proposal_title' => '<snippet_undefined><snippet_keyword>%1%</snippet_keyword> has missing steps. Define them with these snippets:</snippet_undefined>',
'snippet_missing_title' => '<snippet_undefined>Use <snippet_keyword>--snippets-for</snippet_keyword> CLI option to generate snippets for following <snippet_keyword>%1%</snippet_keyword> suite steps:</snippet_undefined>',
'skipped_scenarios_title' => 'Skipped scenarios:',
'failed_scenarios_title' => 'Failed scenarios:',
'failed_hooks_title' => 'Failed hooks:',
'failed_steps_title' => 'Failed steps:',
'pending_steps_title' => 'Pending steps:',
'scenarios_count' => '{0} No scenarios|{1} 1 scenario|]1,Inf] %1% scenarios',
'steps_count' => '{0} No steps|{1} 1 step|]1,Inf] %1% steps',
'passed_count' => '[1,Inf] %1% passed',
'failed_count' => '[1,Inf] %1% failed',
'pending_count' => '[1,Inf] %1% pending',
'undefined_count' => '[1,Inf] %1% undefined',
'skipped_count' => '[1,Inf] %1% skipped',
),
'cs' => array(
'snippet_proposal_title' => '<snippet_undefined><snippet_keyword>%1%</snippet_keyword> obsahuje chybné kroky. Definujte je za použití následujícího kódu:</snippet_undefined>',
'snippet_missing_title' => '<snippet_undefined>Snippety pro následující kroky v sadě <snippet_keyword>%1%</snippet_keyword> nebyly vygenerovány (zkontrolujte správnost konfigurace):</snippet_undefined>',
'failed_scenarios_title' => 'Chybné scénáře:',
'failed_hooks_title' => 'Chybné hooky:',
'failed_steps_title' => 'Chybné kroky:',
'pending_steps_title' => 'Čekající kroky:',
'scenarios_count' => '{0} Žádný scénář|{1} 1 scénář|{2,3,4} %1% scénáře|]4,Inf] %1% scénářů',
'steps_count' => '{0} Žádné kroky|{1} 1 krok|{2,3,4} %1% kroky|]4,Inf] %1% kroků',
'passed_count' => '{1} %1% prošel|{2,3,4} %1% prošly|]4,Inf] %1% prošlo',
'failed_count' => '{1} %1% selhal|{2,3,4} %1% selhaly|]4,Inf] %1% selhalo',
'pending_count' => '{1} %1% čeká|{2,3,4} %1% čekají|]4,Inf] %1% čeká',
'undefined_count' => '{1} %1% nedefinován|{2,3,4} %1% nedefinovány|]4,Inf] %1% nedefinováno',
'skipped_count' => '{1} %1% přeskočen|{2,3,4} %1% přeskočeny|]4,Inf] %1% přeskočeno',
),
'de' => array(
'snippet_proposal_title' => '<snippet_undefined><snippet_keyword>%1%</snippet_keyword> hat fehlende Schritte. Definiere diese mit den folgenden Snippets:</snippet_undefined>',
'snippet_missing_title' => '<snippet_undefined>Snippets für die folgenden Schritte in der <snippet_keyword>%1%</snippet_keyword> Suite wurden nicht generiert (Konfiguration überprüfen):</snippet_undefined>',
'failed_scenarios_title' => 'Fehlgeschlagene Szenarien:',
'failed_hooks_title' => 'Fehlgeschlagene Hooks:',
'failed_steps_title' => 'Fehlgeschlagene Schritte:',
'pending_steps_title' => 'Ausstehende Schritte:',
'scenarios_count' => '{0} Kein Szenario|{1} 1 Szenario|]1,Inf] %1% Szenarien',
'steps_count' => '{0} Kein Schritt|{1} 1 Schritt|]1,Inf] %1% Schritte',
'passed_count' => '[1,Inf] %1% bestanden',
'failed_count' => '[1,Inf] %1% fehlgeschlagen',
'pending_count' => '[1,Inf] %1% ausstehend',
'undefined_count' => '[1,Inf] %1% nicht definiert',
'skipped_count' => '[1,Inf] %1% übersprungen',
),
'es' => array(
'snippet_proposal_title' => '<snippet_undefined>A <snippet_keyword>%1%</snippet_keyword> le faltan pasos. Defínelos con estos pasos:</snippet_undefined>',
'snippet_missing_title' => '<snippet_undefined>Las plantillas para los siguientes pasos en <snippet_keyword>%1%</snippet_keyword> no fueron generadas (revisa tu configuración):</snippet_undefined>',
'failed_scenarios_title' => 'Escenarios fallidos:',
'failed_hooks_title' => 'Hooks fallidos:',
'failed_steps_title' => 'Pasos fallidos:',
'pending_steps_title' => 'Pasos pendientes:',
'scenarios_count' => '{0} Ningún escenario|{1} 1 escenario|]1,Inf] %1% escenarios',
'steps_count' => '{0} Ningún paso|{1} 1 paso|]1,Inf] %1% pasos',
'passed_count' => '[1,Inf] %1% pasaron',
'failed_count' => '[1,Inf] %1% fallaron',
'pending_count' => '[1,Inf] %1% pendientes',
'undefined_count' => '[1,Inf] %1% por definir',
'skipped_count' => '[1,Inf] %1% saltadas',
),
'fr' => array(
'snippet_proposal_title' => '<snippet_undefined><snippet_keyword>%1%</snippet_keyword> a des étapes manquantes. Définissez-les avec les modèles suivants :</snippet_undefined>',
'snippet_missing_title' => '<snippet_undefined>Les modèles des étapes de la suite <snippet_keyword>%1%</snippet_keyword> n\'ont pas été générés (vérifiez votre configuration):</snippet_undefined>',
'failed_scenarios_title' => 'Scénarios échoués:',
'failed_hooks_title' => 'Hooks échoués:',
'failed_steps_title' => 'Etapes échouées:',
'pending_steps_title' => 'Etapes en attente:',
'scenarios_count' => '{0} Pas de scénario|{1} 1 scénario|]1,Inf] %1% scénarios',
'steps_count' => '{0} Pas d\'étape|{1} 1 étape|]1,Inf] %1% étapes',
'passed_count' => '[1,Inf] %1% succès',
'failed_count' => '[1,Inf] %1% échecs',
'pending_count' => '[1,Inf] %1% en attente',
'undefined_count' => '[1,Inf] %1% indéfinis',
'skipped_count' => '[1,Inf] %1% ignorés',
),
'it' => array(
'snippet_proposal_title' => '<snippet_undefined><snippet_keyword>%1%</snippet_keyword> ha dei passaggi mancanti. Definiscili con questi snippet:</snippet_undefined>',
'snippet_missing_title' => '<snippet_undefined>Gli snippet per i seguenti passaggi della suite <snippet_keyword>%1%</snippet_keyword> non sono stati generati (verifica la configurazione):</snippet_undefined>',
'failed_scenarios_title' => 'Scenari falliti:',
'failed_hooks_title' => 'Hook falliti:',
'failed_steps_title' => 'Passaggi falliti:',
'pending_steps_title' => 'Passaggi in sospeso:',
'scenarios_count' => '{0} Nessuno scenario|{1} 1 scenario|]1,Inf] %1% scenari',
'steps_count' => '{0} Nessun passaggio|{1} 1 passaggio|]1,Inf] %1% passaggi',
'passed_count' => '{1} 1 superato|]1,Inf] %1% superati',
'failed_count' => '{1} 1 fallito|]1,Inf] %1% falliti',
'pending_count' => '[1,Inf] %1% in sospeso',
'undefined_count' => '{1} 1 non definito|]1,Inf] %1% non definiti',
'skipped_count' => '{1} 1 ignorato|]1,Inf] %1% ignorati',
),
'ja' => array(
'snippet_proposal_title' => '<snippet_undefined><snippet_keyword>%1%</snippet_keyword> のステップが見つかりません。 次のスニペットで定義できます:</snippet_undefined>',
'snippet_missing_title' => '<snippet_undefined>以下のステップのスニペットは<snippet_keyword>%1%</snippet_keyword>スイートに生成されませんでした(設定を確認してください):</snippet_undefined>',
'skipped_scenarios_title' => 'スキップした シナリオ:',
'failed_scenarios_title' => '失敗した シナリオ:',
'failed_hooks_title' => '失敗した フック:',
'failed_steps_title' => '失敗した ステップ:',
'pending_steps_title' => '保留中のステップ:',
'scenarios_count' => '{0} No scenarios|{1} 1 個のシナリオ|]1,Inf] %1% 個のシナリオ',
'steps_count' => '{0} ステップがありません|{1} 1 個のステップ|]1,Inf] %1% 個のステップ',
'passed_count' => '[1,Inf] %1% 個成功',
'failed_count' => '[1,Inf] %1% 個失敗',
'pending_count' => '[1,Inf] %1% 個保留',
'undefined_count' => '[1,Inf] %1% 個未定義',
'skipped_count' => '[1,Inf] %1% 個スキップ',
),
'nl' => array(
'snippet_proposal_title' => '<snippet_undefined>Ontbrekende stappen in <snippet_keyword>%1%</snippet_keyword>. Definieer ze met de volgende fragmenten:</snippet_undefined>',
'snippet_missing_title' => '<snippet_undefined>Fragmenten voor de volgende stappen in de <snippet_keyword>%1%</snippet_keyword> suite werden niet gegenereerd (controleer de configuratie):</snippet_undefined>',
'failed_scenarios_title' => 'Gefaalde scenario\'s:',
'failed_hooks_title' => 'Gefaalde hooks:',
'failed_steps_title' => 'Gefaalde stappen:',
'pending_steps_title' => 'Onafgewerkte stappen:',
'scenarios_count' => '{0} Geen scenario\'s|{1} 1 scenario|]1,Inf] %1% scenario\'s',
'steps_count' => '{0} Geen stappen|{1} 1 stap|]1,Inf] %1% stappen',
'passed_count' => '[1,Inf] %1% geslaagd',
'failed_count' => '[1,Inf] %1% gefaald',
'pending_count' => '[1,Inf] %1% wachtende',
'undefined_count' => '[1,Inf] %1% niet gedefinieerd',
'skipped_count' => '[1,Inf] %1% overgeslagen',
),
'no' => array(
'snippet_proposal_title' => '<snippet_undefined><snippet_keyword>%1%</snippet_keyword> mangler steg. Definer dem med disse snuttene:</snippet_undefined>',
'snippet_missing_title' => '<snippet_undefined>Snutter for de følgende stegene i <snippet_keyword>%1%</snippet_keyword>-samlingen ble ikke laget. (Sjekk konfigurasjonen din.):</snippet_undefined>',
'failed_scenarios_title' => 'Feilende scenarier:',
'failed_hooks_title' => 'Feilende hooks:',
'failed_steps_title' => 'Feilende steg:',
'pending_steps_title' => 'Ikke implementerte steg:',
'scenarios_count' => '{0} Ingen scenarier|{1} 1 scenario|]1,Inf] %1% scenarier',
'steps_count' => '{0} Ingen steg|{1} 1 steg|]1,Inf] %1% steg',
'passed_count' => '[1,Inf] %1% ok',
'failed_count' => '[1,Inf] %1% feilet',
'pending_count' => '[1,Inf] %1% ikke implementert',
'undefined_count' => '[1,Inf] %1% ikke definert',
'skipped_count' => '[1,Inf] %1% hoppet over',
),
'pl' => array(
'snippet_proposal_title' => '<snippet_undefined><snippet_keyword>%1%</snippet_keyword> zawiera brakujące kroki. Utwórz je korzystając z tych fragmentów kodu:</snippet_undefined>',
'snippet_missing_title' => '<snippet_undefined>Fragmenty kodu dla następujących kroków <snippet_keyword>%1%</snippet_keyword> nie zostały wygenerowane (sprawdź swoją konfigurację):</snippet_undefined>',
'failed_scenarios_title' => 'Nieudane scenariusze:',
'failed_hooks_title' => 'Nieudane hooki:',
'failed_steps_title' => 'Nieudane kroki',
'pending_steps_title' => 'Oczekujące kroki',
'scenarios_count' => '{0} Brak scenariuszy|{1} 1 scenariusz|{2,3,4,22,23,24,32,33,34,42,43,44} %1% scenariusze|]4,Inf] %1% scenariuszy',
'steps_count' => '{0} Brak kroków|{1} 1 krok|{2,3,4,22,23,24,32,33,34,42,43,44} %1% kroki|]4,Inf] %1% kroków',
'passed_count' => '{1} %1% udany|{2,3,4,22,23,24,32,33,34,42,43,44} %1% udane|]4,Inf] %1% udanych',
'failed_count' => '{1} %1% nieudany|{2,3,4,22,23,24,32,33,34,42,43,44} %1% nieudane|]4,Inf] %1% nieudanych',
'pending_count' => '{1} %1% oczekujący|{2,3,4,22,23,24,32,33,34,42,43,44} %1% oczekujące|]4,Inf] %1% oczekujących',
'undefined_count' => '{1} %1% niezdefiniowany|{2,3,4,22,23,24,32,33,34,42,43,44} %1% niezdefiniowane|]4,Inf] %1% niezdefiniowanych',
'skipped_count' => '{1} %1% pominięty|{2,3,4,22,23,24,32,33,34,42,43,44} %1% pominięte|]4,Inf] %1% pominiętych',
),
'pt' => array(
'snippet_proposal_title' => '<snippet_undefined><snippet_keyword>%1%</snippet_keyword> contém definições em falta. Defina-as com estes exemplos:</snippet_undefined>',
'snippet_missing_title' => '<snippet_undefined>Os exemplos para as seguintes definições da suite <snippet_keyword>%1%</snippet_keyword> não foram gerados (verifique a configuração):</snippet_undefined>',
'failed_scenarios_title' => 'Cenários que falharam:',
'failed_hooks_title' => 'Hooks que falharam:',
'failed_steps_title' => 'Definições que falharam:',
'pending_steps_title' => 'Definições por definir:',
'scenarios_count' => '{0} Nenhum cenário|{1} 1 cenário|]1,Inf] %1% cenários',
'steps_count' => '{0} Nenhuma definição|{1} 1 definição|]1,Inf] %1% definições',
'passed_count' => '{1} passou|]1,Inf] %1% passaram',
'failed_count' => '{1} falhou|]1,Inf] %1% falharam',
'pending_count' => '[1,Inf] %1% por definir',
'undefined_count' => '{1} indefinido|]1,Inf] %1% indefinidos',
'skipped_count' => '{1} omitido|]1,Inf] %1% omitidos',
),
'pt-BR' => array(
'snippet_proposal_title' => '<snippet_undefined><snippet_keyword>%1%</snippet_keyword> possue etapas faltando. Defina elas com esse(s) trecho(s) de código:</snippet_undefined>',
'snippet_missing_title' => '<snippet_undefined>Trecho de códigos para as seguintes etapas em <snippet_keyword>%1%</snippet_keyword> suite não foram geradas (verique sua configuração):</snippet_undefined>',
'failed_scenarios_title' => 'Cenários falhados:',
'failed_hooks_title' => 'Hooks falhados:',
'failed_steps_title' => 'Etapas falhadas:',
'pending_steps_title' => 'Etapas pendentes:',
'scenarios_count' => '{0} Nenhum cenário|{1} 1 cenário|]1,Inf] %1% cenários',
'steps_count' => '{0} Nenhuma etapa|{1} 1 etapa|]1,Inf] %1% etapas',
'passed_count' => '[1,Inf] %1% passou',
'failed_count' => '[1,Inf] %1% falhou',
'pending_count' => '[1,Inf] %1% pendente',
'undefined_count' => '[1,Inf] %1% indefinido',
'skipped_count' => '[1,Inf] %1% pulado',
),
'ro' => array(
'snippet_proposal_title' => '<snippet_undefined><snippet_keyword>%1%</snippet_keyword> are pași lipsa. Puteți implementa pașii cu ajutorul acestor fragmente de cod:</snippet_undefined>',
'snippet_missing_title' => '<snippet_undefined>Fragmentele de cod pentru urmatorii pași din suita <snippet_keyword>%1%</snippet_keyword> nu au fost generate (contextul tau implementeaza interfata SnippetAcceptingContext?):</snippet_undefined>',
'skipped_scenarios_title' => 'Scenarii omise:',
'failed_scenarios_title' => 'Scenarii eșuate:',
'failed_hooks_title' => 'Hook-uri eșuate:',
'failed_steps_title' => 'Pași esuați:',
'pending_steps_title' => 'Pași in așteptare:',
'scenarios_count' => '{0} Niciun scenariu|{1} 1 scenariu|]1,Inf] %1% scenarii',
'steps_count' => '{0} Niciun pas|{1} 1 pas|]1,Inf] %1% pasi',
'passed_count' => '[1,Inf] %1% cu succes',
'failed_count' => '[1,Inf] %1% fara success',
'pending_count' => '[1,Inf] %1% in așteptare',
'undefined_count' => '[1,Inf] %1% fara implementare',
'skipped_count' => '{1} %1% omis|]1,Inf] %1% omiși',
),
'ru' => array(
'snippet_proposal_title' => '<snippet_keyword>%1%</snippet_keyword> <snippet_undefined>не содержит необходимых определений. Вы можете добавить их используя шаблоны:</snippet_undefined>',
'snippet_missing_title' => '<snippet_undefined>Шаблоны для следующих шагов в среде <snippet_keyword>%1%</snippet_keyword> не были сгенерированы (проверьте ваши настройки):</snippet_undefined>',
'skipped_scenarios_title' => 'Пропущенные сценарии:',
'failed_scenarios_title' => 'Проваленные сценарии:',
'failed_hooks_title' => 'Проваленные хуки:',
'failed_steps_title' => 'Проваленные шаги:',
'pending_steps_title' => 'Шаги в ожидании:',
'scenarios_count' => '{0} Нет сценариев|{1,21,31} %1% сценарий|{2,3,4,22,23,24} %1% сценария|]4,Inf] %1% сценариев',
'steps_count' => '{0} Нет шагов|{1,21,31} %1% шаг|{2,3,4,22,23,24} %1% шага|]4,Inf] %1% шагов',
'passed_count' => '{1,21,31} %1% пройден|]1,Inf] %1% пройдено',
'failed_count' => '{1,21,31} %1% провален|]1,Inf] %1% провалено',
'pending_count' => '[1,Inf] %1% в ожидании',
'undefined_count' => '{1,21,31} %1% не определен|]1,Inf] %1% не определено',
'skipped_count' => '{1,21,31} %1% пропущен|]1,Inf] %1% пропущено',
),
);

View file

@ -0,0 +1,146 @@
<?php
/*
* This file is part of the Behat.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Behat\Behat;
use Behat\Behat\Context\ServiceContainer\ContextExtension;
use Behat\Behat\Definition\ServiceContainer\DefinitionExtension;
use Behat\Behat\EventDispatcher\ServiceContainer\EventDispatcherExtension;
use Behat\Behat\Gherkin\ServiceContainer\GherkinExtension;
use Behat\Behat\Hook\ServiceContainer\HookExtension;
use Behat\Behat\Output\ServiceContainer\Formatter\JUnitFormatterFactory;
use Behat\Behat\Output\ServiceContainer\Formatter\PrettyFormatterFactory;
use Behat\Behat\Output\ServiceContainer\Formatter\ProgressFormatterFactory;
use Behat\Behat\HelperContainer\ServiceContainer\HelperContainerExtension;
use Behat\Behat\Snippet\ServiceContainer\SnippetExtension;
use Behat\Behat\Tester\ServiceContainer\TesterExtension;
use Behat\Behat\Transformation\ServiceContainer\TransformationExtension;
use Behat\Behat\Translator\ServiceContainer\GherkinTranslationsExtension;
use Behat\Testwork\ApplicationFactory as BaseFactory;
use Behat\Testwork\Argument\ServiceContainer\ArgumentExtension;
use Behat\Testwork\Autoloader\ServiceContainer\AutoloaderExtension;
use Behat\Testwork\Call\ServiceContainer\CallExtension;
use Behat\Testwork\Cli\ServiceContainer\CliExtension;
use Behat\Testwork\Environment\ServiceContainer\EnvironmentExtension;
use Behat\Testwork\Exception\ServiceContainer\ExceptionExtension;
use Behat\Testwork\Filesystem\ServiceContainer\FilesystemExtension;
use Behat\Testwork\Ordering\ServiceContainer\OrderingExtension;
use Behat\Testwork\Output\ServiceContainer\Formatter\FormatterFactory;
use Behat\Testwork\Output\ServiceContainer\OutputExtension;
use Behat\Testwork\ServiceContainer\ServiceProcessor;
use Behat\Testwork\Specification\ServiceContainer\SpecificationExtension;
use Behat\Testwork\Suite\ServiceContainer\SuiteExtension;
use Behat\Testwork\Translator\ServiceContainer\TranslatorExtension;
/**
* Defines the way behat is created.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
final class ApplicationFactory extends BaseFactory
{
const VERSION = '3.3.0';
/**
* {@inheritdoc}
*/
protected function getName()
{
return 'behat';
}
/**
* {@inheritdoc}
*/
protected function getVersion()
{
return self::VERSION;
}
/**
* {@inheritdoc}
*/
protected function getDefaultExtensions()
{
$processor = new ServiceProcessor();
return array(
new ArgumentExtension(),
new AutoloaderExtension(array('' => '%paths.base%/features/bootstrap')),
new SuiteExtension($processor),
new OutputExtension('pretty', $this->getDefaultFormatterFactories($processor), $processor),
new ExceptionExtension($processor),
new GherkinExtension($processor),
new CallExtension($processor),
new TranslatorExtension(),
new GherkinTranslationsExtension(),
new TesterExtension($processor),
new CliExtension($processor),
new EnvironmentExtension($processor),
new SpecificationExtension($processor),
new FilesystemExtension(),
new ContextExtension($processor),
new SnippetExtension($processor),
new DefinitionExtension($processor),
new EventDispatcherExtension($processor),
new HookExtension(),
new TransformationExtension($processor),
new OrderingExtension($processor),
new HelperContainerExtension($processor)
);
}
/**
* {@inheritdoc}
*/
protected function getEnvironmentVariableName()
{
return 'BEHAT_PARAMS';
}
/**
* {@inheritdoc}
*/
protected function getConfigPath()
{
$cwd = rtrim(getcwd(), DIRECTORY_SEPARATOR);
$paths = array_filter(
array(
$cwd . DIRECTORY_SEPARATOR . 'behat.yml',
$cwd . DIRECTORY_SEPARATOR . 'behat.yml.dist',
$cwd . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . 'behat.yml',
$cwd . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . 'behat.yml.dist',
),
'is_file'
);
if (count($paths)) {
return current($paths);
}
return null;
}
/**
* Returns default formatter factories.
*
* @param ServiceProcessor $processor
*
* @return FormatterFactory[]
*/
private function getDefaultFormatterFactories(ServiceProcessor $processor)
{
return array(
new PrettyFormatterFactory($processor),
new ProgressFormatterFactory($processor),
new JUnitFormatterFactory(),
);
}
}

View file

@ -0,0 +1,37 @@
<?php
/*
* This file is part of the Behat.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Behat\Behat\Context\Annotation;
use Behat\Behat\Context\Reader\AnnotatedContextReader;
use Behat\Testwork\Call\Callee;
use ReflectionMethod;
/**
* Reads custom annotation of a provided context method into a Callee.
*
* @see AnnotatedContextReader
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
interface AnnotationReader
{
/**
* Reads all callees associated with a provided method.
*
* @param string $contextClass
* @param ReflectionMethod $method
* @param string $docLine
* @param string $description
*
* @return null|Callee
*/
public function readCallee($contextClass, ReflectionMethod $method, $docLine, $description);
}

View file

@ -0,0 +1,34 @@
<?php
/*
* This file is part of the Behat.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Behat\Behat\Context\Argument;
use Behat\Behat\Context\Environment\Handler\ContextEnvironmentHandler;
use ReflectionClass;
/**
* Resolves arguments of context constructors.
*
* @see ContextEnvironmentHandler
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
interface ArgumentResolver
{
/**
* Resolves context constructor arguments.
*
* @param ReflectionClass $classReflection
* @param mixed[] $arguments
*
* @return mixed[]
*/
public function resolveArguments(ReflectionClass $classReflection, array $arguments);
}

View file

@ -0,0 +1,52 @@
<?php
/*
* This file is part of the Behat.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Behat\Behat\Context\Argument;
use Behat\Testwork\Suite\Suite;
/**
* Composite factory. Delegates to other (registered) factories to do the job.
*
* @see ContextEnvironmentHandler
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
final class CompositeFactory implements SuiteScopedResolverFactory
{
/**
* @var SuiteScopedResolverFactory[]
*/
private $factories = array();
/**
* Registers factory.
*
* @param SuiteScopedResolverFactory $factory
*/
public function registerFactory(SuiteScopedResolverFactory $factory)
{
$this->factories[] = $factory;
}
/**
* {@inheritdoc}
*/
public function generateArgumentResolvers(Suite $suite)
{
return array_reduce(
$this->factories,
function (array $resolvers, SuiteScopedResolverFactory $factory) use ($suite) {
return array_merge($resolvers, $factory->generateArgumentResolvers($suite));
},
array()
);
}
}

View file

@ -0,0 +1,31 @@
<?php
/*
* This file is part of the Behat.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Behat\Behat\Context\Argument;
use Behat\Testwork\Suite\Suite;
/**
* NoOp factory. Always returns zero resolvers.
*
* @see ContextEnvironmentHandler
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
final class NullFactory implements SuiteScopedResolverFactory
{
/**
* {@inheritdoc}
*/
public function generateArgumentResolvers(Suite $suite)
{
return array();
}
}

View file

@ -0,0 +1,32 @@
<?php
/*
* This file is part of the Behat.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Behat\Behat\Context\Argument;
use Behat\Testwork\Suite\Suite;
/**
* Creates argument resolvers for provided suite.
*
* @see ContextEnvironmentHandler
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
interface SuiteScopedResolverFactory
{
/**
* Creates argument resolvers for provided suite.
*
* @param Suite $suite
*
* @return ArgumentResolver[]
*/
public function generateArgumentResolvers(Suite $suite);
}

View file

@ -0,0 +1,91 @@
<?php
/*
* This file is part of the Behat.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Behat\Behat\Context\Cli;
use Behat\Behat\Context\Snippet\Generator\AggregatePatternIdentifier;
use Behat\Behat\Context\Snippet\Generator\ContextInterfaceBasedContextIdentifier;
use Behat\Behat\Context\Snippet\Generator\ContextInterfaceBasedPatternIdentifier;
use Behat\Behat\Context\Snippet\Generator\ContextSnippetGenerator;
use Behat\Behat\Context\Snippet\Generator\FixedContextIdentifier;
use Behat\Behat\Context\Snippet\Generator\FixedPatternIdentifier;
use Behat\Behat\Context\Snippet\Generator\AggregateContextIdentifier;
use Behat\Testwork\Cli\Controller;
use Symfony\Component\Console\Command\Command as SymfonyCommand;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Translation\TranslatorInterface;
/**
* Configures which context snippets are generated for.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
final class ContextSnippetsController implements Controller
{
/**
* @var ContextSnippetGenerator
*/
private $generator;
/**
* @var TranslatorInterface
*/
private $translator;
/**
* Initialises controller.
*
* @param ContextSnippetGenerator $generator
* @param TranslatorInterface $translator
*/
public function __construct(ContextSnippetGenerator $generator, TranslatorInterface $translator)
{
$this->generator = $generator;
$this->translator = $translator;
}
/**
* {@inheritdoc}
*/
public function configure(SymfonyCommand $command)
{
$command
->addOption(
'--snippets-for', null, InputOption::VALUE_OPTIONAL,
"Specifies which context class to generate snippets for."
)
->addOption(
'--snippets-type', null, InputOption::VALUE_REQUIRED,
"Specifies which type of snippets (turnip, regex) to generate."
);
}
/**
* {@inheritdoc}
*/
public function execute(InputInterface $input, OutputInterface $output)
{
$this->generator->setContextIdentifier(
new AggregateContextIdentifier(array(
new ContextInterfaceBasedContextIdentifier(),
new FixedContextIdentifier($input->getOption('snippets-for')),
new InteractiveContextIdentifier($this->translator, $input, $output)
))
);
$this->generator->setPatternIdentifier(
new AggregatePatternIdentifier(array(
new ContextInterfaceBasedPatternIdentifier(),
new FixedPatternIdentifier($input->getOption('snippets-type'))
))
);
}
}

View file

@ -0,0 +1,114 @@
<?php
/*
* This file is part of the Behat.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Behat\Behat\Context\Cli;
use Behat\Behat\Context\Environment\ContextEnvironment;
use Behat\Behat\Context\Snippet\Generator\TargetContextIdentifier;
use Symfony\Component\Console\Helper\QuestionHelper;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\ChoiceQuestion;
use Symfony\Component\Translation\TranslatorInterface;
/**
* Interactive identifier that asks user for input.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
final class InteractiveContextIdentifier implements TargetContextIdentifier
{
/**
* @var TranslatorInterface
*/
private $translator;
/**
* @var InputInterface
*/
private $input;
/**
* @var OutputInterface
*/
private $output;
/**
* Initialises identifier.
*
* @param TranslatorInterface $translator
* @param InputInterface $input
* @param OutputInterface $output
*/
public function __construct(TranslatorInterface $translator, InputInterface $input, OutputInterface $output)
{
$this->translator = $translator;
$this->input = $input;
$this->output = $output;
}
/**
* {@inheritdoc}
*/
public function guessTargetContextClass(ContextEnvironment $environment)
{
if ($this->interactionIsNotSupported()) {
return null;
}
$suiteName = $environment->getSuite()->getName();
$contextClasses = $environment->getContextClasses();
if (!count($contextClasses)) {
return null;
}
$message = $this->translator->trans('snippet_context_choice', array('%1%' => $suiteName), 'output');
$choices = array_values(array_merge(array('None'), $contextClasses));
$default = current($contextClasses);
$answer = $this->askQuestion('>> ' . $message, $choices, $default);
return 'None' !== $answer ? $answer : null;
}
/**
* Asks user question.
*
* @param string $message
* @param string[] $choices
* @param string $default
*
* @return string
*/
private function askQuestion($message, $choices, $default)
{
$this->output->writeln('');
$helper = new QuestionHelper();
$question = new ChoiceQuestion(' ' . $message . "\n", $choices, $default);
return $helper->ask($this->input, $this->output, $question);
}
/**
* Checks if interactive mode is supported.
*
* @return Boolean
*
* @deprecated there is a better way to do it - `InputInterface::isInteractive()` method.
* Sadly, this doesn't work properly prior Symfony\Console 2.7 and as we need
* to support 2.5+ until the next major, we are forced to do a more explicit
* check for the CLI option. This should be reverted back to proper a
* `InputInterface::isInteractive()` call as soon as we bump dependencies
* to Symfony\Console 3.x in Behat 4.x.
*/
private function interactionIsNotSupported()
{
return $this->input->hasParameterOption('--no-interaction');
}
}

View file

@ -0,0 +1,20 @@
<?php
/*
* This file is part of the Behat.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Behat\Behat\Context;
/**
* Marks a custom user-defined class as a behat context.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
interface Context
{
}

View file

@ -0,0 +1,44 @@
<?php
/*
* This file is part of the Behat.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Behat\Behat\Context\ContextClass;
use Behat\Behat\Context\Suite\Setup\SuiteWithContextsSetup;
use Behat\Testwork\Suite\Suite;
/**
* Generates context classes (as a string).
*
* @see SuiteWithContextsSetup
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
interface ClassGenerator
{
/**
* Checks if generator supports provided context class.
*
* @param Suite $suite
* @param string $contextClass
*
* @return Boolean
*/
public function supportsSuiteAndClass(Suite $suite, $contextClass);
/**
* Generates context class code.
*
* @param Suite $suite
* @param string $contextClass
*
* @return string The context class source code
*/
public function generateClass(Suite $suite, $contextClass);
}

View file

@ -0,0 +1,41 @@
<?php
/*
* This file is part of the Behat.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Behat\Behat\Context\ContextClass;
use Behat\Behat\Context\Environment\Handler\ContextEnvironmentHandler;
/**
* Resolves arbitrary context strings into a context classes.
*
* @see ContextEnvironmentHandler
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
interface ClassResolver
{
/**
* Checks if resolvers supports provided class.
*
* @param string $contextString
*
* @return Boolean
*/
public function supportsClass($contextString);
/**
* Resolves context class.
*
* @param string $contextClass
*
* @return string
*/
public function resolveClass($contextClass);
}

View file

@ -0,0 +1,80 @@
<?php
/*
* This file is part of the Behat.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Behat\Behat\Context\ContextClass;
use Behat\Testwork\Suite\Suite;
/**
* Generates basic PHP 5.3+ class with an optional namespace.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
final class SimpleClassGenerator implements ClassGenerator
{
/**
* @var string
*/
protected static $template = <<<'PHP'
<?php
{namespace}use Behat\Behat\Context\Context;
use Behat\Gherkin\Node\PyStringNode;
use Behat\Gherkin\Node\TableNode;
/**
* Defines application features from the specific context.
*/
class {className} implements Context
{
/**
* Initializes context.
*
* Every scenario gets its own context instance.
* You can also pass arbitrary arguments to the
* context constructor through behat.yml.
*/
public function __construct()
{
}
}
PHP;
/**
* {@inheritdoc}
*/
public function supportsSuiteAndClass(Suite $suite, $contextClass)
{
return true;
}
/**
* {@inheritdoc}
*/
public function generateClass(Suite $suite, $contextClass)
{
$fqn = $contextClass;
$namespace = '';
if (false !== $pos = strrpos($fqn, '\\')) {
$namespace = 'namespace ' . substr($fqn, 0, $pos) . ";\n\n";
$contextClass = substr($fqn, $pos + 1);
}
return strtr(
static::$template,
array(
'{namespace}' => $namespace,
'{className}' => $contextClass,
)
);
}
}

View file

@ -0,0 +1,140 @@
<?php
/*
* This file is part of the Behat.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Behat\Behat\Context;
use Behat\Behat\Context\Argument\ArgumentResolver;
use Behat\Behat\Context\Initializer\ContextInitializer;
use Behat\Testwork\Argument\ArgumentOrganiser;
use ReflectionClass;
/**
* Instantiates contexts using registered argument resolvers and context initializers.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
final class ContextFactory
{
/**
* @var ArgumentOrganiser
*/
private $argumentOrganiser;
/**
* @var ArgumentResolver[]
*/
private $argumentResolvers = array();
/**
* @var ContextInitializer[]
*/
private $contextInitializers = array();
/**
* Initialises factory.
*
* @param ArgumentOrganiser $argumentOrganiser
*/
public function __construct(ArgumentOrganiser $argumentOrganiser)
{
$this->argumentOrganiser = $argumentOrganiser;
}
/**
* Registers context argument resolver.
*
* @param ArgumentResolver $resolver
*/
public function registerArgumentResolver(ArgumentResolver $resolver)
{
$this->argumentResolvers[] = $resolver;
}
/**
* Registers context initializer.
*
* @param ContextInitializer $initializer
*/
public function registerContextInitializer(ContextInitializer $initializer)
{
$this->contextInitializers[] = $initializer;
}
/**
* Creates and initializes context class.
*
* @param string $class
* @param array $arguments
* @param ArgumentResolver[] $singleUseResolvers
*
* @return Context
*/
public function createContext($class, array $arguments = array(), array $singleUseResolvers = array())
{
$reflection = new ReflectionClass($class);
$resolvers = array_merge($singleUseResolvers, $this->argumentResolvers);
$resolvedArguments = $this->resolveArguments($reflection, $arguments, $resolvers);
$context = $this->createInstance($reflection, $resolvedArguments);
$this->initializeInstance($context);
return $context;
}
/**
* Resolves arguments for a specific class using registered argument resolvers.
*
* @param ReflectionClass $reflection
* @param array $arguments
* @param ArgumentResolver[] $resolvers
*
* @return mixed[]
*/
private function resolveArguments(ReflectionClass $reflection, array $arguments, array $resolvers)
{
foreach ($resolvers as $resolver) {
$arguments = $resolver->resolveArguments($reflection, $arguments);
}
if (!$reflection->hasMethod('__construct') || !count($arguments)) {
return $arguments;
}
$constructor = $reflection->getConstructor();
return $this->argumentOrganiser->organiseArguments($constructor, $arguments);
}
/**
* Creates context instance.
*
* @param ReflectionClass $reflection
* @param array $arguments
*
* @return mixed
*/
private function createInstance(ReflectionClass $reflection, array $arguments)
{
if (count($arguments)) {
return $reflection->newInstanceArgs($arguments);
}
return $reflection->newInstance();
}
/**
* Initializes context class and returns new context instance.
*
* @param Context $context
*/
private function initializeInstance(Context $context)
{
foreach ($this->contextInitializers as $initializer) {
$initializer->initializeContext($context);
}
}
}

View file

@ -0,0 +1,34 @@
<?php
/*
* This file is part of the Behat.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Behat\Behat\Context;
use Behat\Behat\Context\Snippet\Generator\ContextSnippetGenerator;
/**
* Context that implements this interface is treated as a custom-snippet-friendly context.
*
* @see ContextSnippetGenerator
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*
* @deprecated will be removed in 4.0. Use --snippets-for and --snippets-type CLI options instead
*/
interface CustomSnippetAcceptingContext extends SnippetAcceptingContext
{
/**
* Returns type of the snippets that this context accepts.
*
* Behat implements a couple of types by default: "regex" and "turnip"
*
* @return string
*/
public static function getAcceptedSnippetType();
}

View file

@ -0,0 +1,47 @@
<?php
/*
* This file is part of the Behat.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Behat\Behat\Context\Environment;
use Behat\Behat\Context\Environment\Handler\ContextEnvironmentHandler;
use Behat\Testwork\Environment\Environment;
/**
* Represents test environment based on a collection of contexts.
*
* @see ContextEnvironmentHandler
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
interface ContextEnvironment extends Environment
{
/**
* Checks if environment has any contexts registered.
*
* @return Boolean
*/
public function hasContexts();
/**
* Returns list of registered context classes.
*
* @return string[]
*/
public function getContextClasses();
/**
* Checks if environment contains context with the specified class name.
*
* @param string $class
*
* @return Boolean
*/
public function hasContextClass($class);
}

View file

@ -0,0 +1,187 @@
<?php
/*
* This file is part of the Behat.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Behat\Behat\Context\Environment\Handler;
use Behat\Behat\Context\Argument\SuiteScopedResolverFactory;
use Behat\Behat\Context\Argument\NullFactory;
use Behat\Behat\Context\ContextClass\ClassResolver;
use Behat\Behat\Context\ContextFactory;
use Behat\Behat\Context\Environment\InitializedContextEnvironment;
use Behat\Behat\Context\Environment\UninitializedContextEnvironment;
use Behat\Testwork\Environment\Environment;
use Behat\Testwork\Environment\Exception\EnvironmentIsolationException;
use Behat\Testwork\Environment\Handler\EnvironmentHandler;
use Behat\Testwork\Suite\Exception\SuiteConfigurationException;
use Behat\Testwork\Suite\Suite;
/**
* Handles build and initialisation of the context-based environments.
*
* @see ContextFactory
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
final class ContextEnvironmentHandler implements EnvironmentHandler
{
/**
* @var ContextFactory
*/
private $contextFactory;
/**
* @var SuiteScopedResolverFactory
*/
private $resolverFactory;
/**
* @var ClassResolver[]
*/
private $classResolvers = array();
/**
* Initializes handler.
*
* @param ContextFactory $factory
* @param SuiteScopedResolverFactory $resolverFactory
*/
public function __construct(ContextFactory $factory, SuiteScopedResolverFactory $resolverFactory = null)
{
$this->contextFactory = $factory;
$this->resolverFactory = $resolverFactory ?: new NullFactory();
}
/**
* Registers context class resolver.
*
* @param ClassResolver $resolver
*/
public function registerClassResolver(ClassResolver $resolver)
{
$this->classResolvers[] = $resolver;
}
/**
* {@inheritdoc}
*/
public function supportsSuite(Suite $suite)
{
return $suite->hasSetting('contexts');
}
/**
* {@inheritdoc}
*/
public function buildEnvironment(Suite $suite)
{
$environment = new UninitializedContextEnvironment($suite);
foreach ($this->getNormalizedContextSettings($suite) as $context) {
$environment->registerContextClass($this->resolveClass($context[0]), $context[1]);
}
return $environment;
}
/**
* {@inheritdoc}
*/
public function supportsEnvironmentAndSubject(Environment $environment, $testSubject = null)
{
return $environment instanceof UninitializedContextEnvironment;
}
/**
* {@inheritdoc}
*/
public function isolateEnvironment(Environment $uninitializedEnvironment, $testSubject = null)
{
if (!$uninitializedEnvironment instanceof UninitializedContextEnvironment) {
throw new EnvironmentIsolationException(sprintf(
'ContextEnvironmentHandler does not support isolation of `%s` environment.',
get_class($uninitializedEnvironment)
), $uninitializedEnvironment);
}
$environment = new InitializedContextEnvironment($uninitializedEnvironment->getSuite());
$resolvers = $this->resolverFactory->generateArgumentResolvers($uninitializedEnvironment->getSuite());
foreach ($uninitializedEnvironment->getContextClassesWithArguments() as $class => $arguments) {
$context = $this->contextFactory->createContext($class, $arguments, $resolvers);
$environment->registerContext($context);
}
return $environment;
}
/**
* Returns normalized suite context settings.
*
* @param Suite $suite
*
* @return array
*/
private function getNormalizedContextSettings(Suite $suite)
{
return array_map(
function ($context) {
$class = $context;
$arguments = array();
if (is_array($context)) {
$class = current(array_keys($context));
$arguments = $context[$class];
}
return array($class, $arguments);
},
$this->getSuiteContexts($suite)
);
}
/**
* Returns array of context classes configured for the provided suite.
*
* @param Suite $suite
*
* @return string[]
*
* @throws SuiteConfigurationException If `contexts` setting is not an array
*/
private function getSuiteContexts(Suite $suite)
{
if (!is_array($suite->getSetting('contexts'))) {
throw new SuiteConfigurationException(
sprintf('`contexts` setting of the "%s" suite is expected to be an array, %s given.',
$suite->getName(),
gettype($suite->getSetting('contexts'))
),
$suite->getName()
);
}
return $suite->getSetting('contexts');
}
/**
* Resolves class using registered class resolvers.
*
* @param string $class
*
* @return string
*/
private function resolveClass($class)
{
foreach ($this->classResolvers as $resolver) {
if ($resolver->supportsClass($class)) {
return $resolver->resolveClass($class);
}
}
return $class;
}
}

View file

@ -0,0 +1,133 @@
<?php
/*
* This file is part of the Behat.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Behat\Behat\Context\Environment;
use Behat\Behat\Context\Context;
use Behat\Behat\Context\Environment\Handler\ContextEnvironmentHandler;
use Behat\Behat\Context\Exception\ContextNotFoundException;
use Behat\Testwork\Call\Callee;
use Behat\Testwork\Suite\Suite;
/**
* Context environment based on a list of instantiated context objects.
*
* @see ContextEnvironmentHandler
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
final class InitializedContextEnvironment implements ContextEnvironment
{
/**
* @var string
*/
private $suite;
/**
* @var Context[]
*/
private $contexts = array();
/**
* Initializes environment.
*
* @param Suite $suite
*/
public function __construct(Suite $suite)
{
$this->suite = $suite;
}
/**
* Registers context instance in the environment.
*
* @param Context $context
*/
public function registerContext(Context $context)
{
$this->contexts[get_class($context)] = $context;
}
/**
* {@inheritdoc}
*/
public function getSuite()
{
return $this->suite;
}
/**
* {@inheritdoc}
*/
public function hasContexts()
{
return count($this->contexts) > 0;
}
/**
* {@inheritdoc}
*/
public function getContextClasses()
{
return array_keys($this->contexts);
}
/**
* {@inheritdoc}
*/
public function hasContextClass($class)
{
return isset($this->contexts[$class]);
}
/**
* Returns list of registered context instances.
*
* @return Context[]
*/
public function getContexts()
{
return array_values($this->contexts);
}
/**
* Returns registered context by its class name.
*
* @param string $class
*
* @return Context
*
* @throws ContextNotFoundException If context is not in the environment
*/
public function getContext($class)
{
if (!$this->hasContextClass($class)) {
throw new ContextNotFoundException(sprintf(
'`%s` context is not found in the suite environment. Have you registered it?',
$class
), $class);
}
return $this->contexts[$class];
}
/**
* {@inheritdoc}
*/
public function bindCallee(Callee $callee)
{
$callable = $callee->getCallable();
if ($callee->isAnInstanceMethod()) {
return array($this->getContext($callable[0]), $callable[1]);
}
return $callable;
}
}

View file

@ -0,0 +1,93 @@
<?php
/*
* This file is part of the Behat.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Behat\Behat\Context\Environment\Reader;
use Behat\Behat\Context\Environment\ContextEnvironment;
use Behat\Behat\Context\Reader\ContextReader;
use Behat\Testwork\Call\Callee;
use Behat\Testwork\Environment\Environment;
use Behat\Testwork\Environment\Exception\EnvironmentReadException;
use Behat\Testwork\Environment\Reader\EnvironmentReader;
/**
* Reads context-based environment callees using registered context loaders.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
final class ContextEnvironmentReader implements EnvironmentReader
{
/**
* @var ContextReader[]
*/
private $contextReaders = array();
/**
* Registers context loader.
*
* @param ContextReader $contextReader
*/
public function registerContextReader(ContextReader $contextReader)
{
$this->contextReaders[] = $contextReader;
}
/**
* {@inheritdoc}
*/
public function supportsEnvironment(Environment $environment)
{
return $environment instanceof ContextEnvironment;
}
/**
* {@inheritdoc}
*/
public function readEnvironmentCallees(Environment $environment)
{
if (!$environment instanceof ContextEnvironment) {
throw new EnvironmentReadException(sprintf(
'ContextEnvironmentReader does not support `%s` environment.',
get_class($environment)
), $environment);
}
$callees = array();
foreach ($environment->getContextClasses() as $contextClass) {
$callees = array_merge(
$callees,
$this->readContextCallees($environment, $contextClass)
);
}
return $callees;
}
/**
* Reads callees from a specific suite's context.
*
* @param ContextEnvironment $environment
* @param string $contextClass
*
* @return Callee[]
*/
private function readContextCallees(ContextEnvironment $environment, $contextClass)
{
$callees = array();
foreach ($this->contextReaders as $loader) {
$callees = array_merge(
$callees,
$loader->readContextCallees($environment, $contextClass)
);
}
return $callees;
}
}

View file

@ -0,0 +1,95 @@
<?php
/*
* This file is part of the Behat.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Behat\Behat\Context\Environment;
use Behat\Behat\Context\Environment\Handler\ContextEnvironmentHandler;
use Behat\Behat\Context\Exception\ContextNotFoundException;
use Behat\Behat\Context\Exception\WrongContextClassException;
use Behat\Testwork\Environment\StaticEnvironment;
/**
* Context environment based on a list of context classes.
*
* @see ContextEnvironmentHandler
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
final class UninitializedContextEnvironment extends StaticEnvironment implements ContextEnvironment
{
/**
* @var array[]
*/
private $contextClasses = array();
/**
* Registers context class.
*
* @param string $contextClass
* @param null|array $arguments
*
* @throws ContextNotFoundException If class does not exist
* @throws WrongContextClassException if class does not implement Context interface
*/
public function registerContextClass($contextClass, array $arguments = null)
{
if (!class_exists($contextClass)) {
throw new ContextNotFoundException(sprintf(
'`%s` context class not found and can not be used.',
$contextClass
), $contextClass);
}
$reflClass = new \ReflectionClass($contextClass);
if (!$reflClass->implementsInterface('Behat\Behat\Context\Context')) {
throw new WrongContextClassException(sprintf(
'Every context class must implement Behat Context interface, but `%s` does not.',
$contextClass
), $contextClass);
}
$this->contextClasses[$contextClass] = $arguments ? : array();
}
/**
* {@inheritdoc}
*/
public function hasContexts()
{
return count($this->contextClasses) > 0;
}
/**
* {@inheritdoc}
*/
public function getContextClasses()
{
return array_keys($this->contextClasses);
}
/**
* {@inheritdoc}
*/
public function hasContextClass($class)
{
return isset($this->contextClasses[$class]);
}
/**
* Returns context classes with their arguments.
*
* @return array[]
*/
public function getContextClassesWithArguments()
{
return $this->contextClasses;
}
}

View file

@ -0,0 +1,22 @@
<?php
/*
* This file is part of the Behat.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Behat\Behat\Context\Exception;
use Behat\Testwork\Exception\TestworkException;
/**
* Represents an exception thrown during context handling.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
interface ContextException extends TestworkException
{
}

View file

@ -0,0 +1,49 @@
<?php
/*
* This file is part of the Behat.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Behat\Behat\Context\Exception;
use InvalidArgumentException;
/**
* Represents an exception thrown when provided context class is not found.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
final class ContextNotFoundException extends InvalidArgumentException implements ContextException
{
/**
* @var string
*/
private $class;
/**
* Initializes exception.
*
* @param string $message
* @param string $class
*/
public function __construct($message, $class)
{
$this->class = $class;
parent::__construct($message);
}
/**
* Returns not found classname.
*
* @return string
*/
public function getClass()
{
return $this->class;
}
}

View file

@ -0,0 +1,49 @@
<?php
/*
* This file is part of the Behat.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Behat\Behat\Context\Exception;
use InvalidArgumentException;
/**
* Represents an exception when provided translation resource is not recognised.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
final class UnknownTranslationResourceException extends InvalidArgumentException implements ContextException
{
/**
* @var string
*/
private $resource;
/**
* Initializes exception.
*
* @param string $message
* @param string $class
*/
public function __construct($message, $class)
{
$this->resource = $class;
parent::__construct($message);
}
/**
* Returns unsupported resource.
*
* @return string
*/
public function getResource()
{
return $this->resource;
}
}

View file

@ -0,0 +1,49 @@
<?php
/*
* This file is part of the Behat.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Behat\Behat\Context\Exception;
use InvalidArgumentException;
/**
* Represents an exception when provided class exists, but is not an acceptable as a context.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
final class WrongContextClassException extends InvalidArgumentException implements ContextException
{
/**
* @var string
*/
private $class;
/**
* Initializes exception.
*
* @param integer $message
* @param string $class
*/
public function __construct($message, $class)
{
$this->class = $class;
parent::__construct($message);
}
/**
* Returns not found classname.
*
* @return string
*/
public function getClass()
{
return $this->class;
}
}

View file

@ -0,0 +1,28 @@
<?php
/*
* This file is part of the Behat.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Behat\Behat\Context\Initializer;
use Behat\Behat\Context\Context;
/**
* Initializes contexts using custom logic.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
interface ContextInitializer
{
/**
* Initializes provided context.
*
* @param Context $context
*/
public function initializeContext(Context $context);
}

View file

@ -0,0 +1,245 @@
<?php
/*
* This file is part of the Behat.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Behat\Behat\Context\Reader;
use Behat\Behat\Context\Annotation\AnnotationReader;
use Behat\Behat\Context\Environment\ContextEnvironment;
use Behat\Testwork\Call\Callee;
use ReflectionClass;
use ReflectionException;
use ReflectionMethod;
/**
* Reads context callees by annotations using registered annotation readers.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
final class AnnotatedContextReader implements ContextReader
{
const DOCLINE_TRIMMER_REGEX = '/^\/\*\*\s*|^\s*\*\s*|\s*\*\/$|\s*$/';
/**
* @var string[]
*/
private static $ignoreAnnotations = array(
'@param',
'@return',
'@throws',
'@see',
'@uses',
'@todo'
);
/**
* @var AnnotationReader[]
*/
private $readers = array();
/**
* Registers annotation reader.
*
* @param AnnotationReader $reader
*/
public function registerAnnotationReader(AnnotationReader $reader)
{
$this->readers[] = $reader;
}
/**
* {@inheritdoc}
*/
public function readContextCallees(ContextEnvironment $environment, $contextClass)
{
$reflection = new ReflectionClass($contextClass);
$callees = array();
foreach ($reflection->getMethods(ReflectionMethod::IS_PUBLIC) as $method) {
foreach ($this->readMethodCallees($reflection->getName(), $method) as $callee) {
$callees[] = $callee;
}
}
return $callees;
}
/**
* Loads callees associated with specific method.
*
* @param string $class
* @param ReflectionMethod $method
*
* @return Callee[]
*/
private function readMethodCallees($class, ReflectionMethod $method)
{
$callees = array();
// read parent annotations
try {
$prototype = $method->getPrototype();
// error occurs on every second PHP stable release - getPrototype() returns itself
if ($prototype->getDeclaringClass()->getName() !== $method->getDeclaringClass()->getName()) {
$callees = array_merge($callees, $this->readMethodCallees($class, $prototype));
}
} catch (ReflectionException $e) {
}
if ($docBlock = $method->getDocComment()) {
$callees = array_merge($callees, $this->readDocBlockCallees($class, $method, $docBlock));
}
return $callees;
}
/**
* Reads callees from the method doc block.
*
* @param string $class
* @param ReflectionMethod $method
* @param string $docBlock
*
* @return Callee[]
*/
private function readDocBlockCallees($class, ReflectionMethod $method, $docBlock)
{
$callees = array();
$description = $this->readDescription($docBlock);
$docBlock = $this->mergeMultilines($docBlock);
foreach (explode("\n", $docBlock) as $docLine) {
$docLine = preg_replace(self::DOCLINE_TRIMMER_REGEX, '', $docLine);
if ($this->isEmpty($docLine)) {
continue;
}
if ($this->isNotAnnotation($docLine)) {
continue;
}
if ($callee = $this->readDocLineCallee($class, $method, $docLine, $description)) {
$callees[] = $callee;
}
}
return $callees;
}
/**
* Merges multiline strings (strings ending with "\")
*
* @param string $docBlock
*
* @return string
*/
private function mergeMultilines($docBlock)
{
return preg_replace("#\\\\$\s*+\*\s*+([^\\\\$]++)#m", '$1', $docBlock);
}
/**
* Extracts a description from the provided docblock,
* with support for multiline descriptions.
*
* @param string $docBlock
*
* @return string
*/
private function readDescription($docBlock)
{
// Remove indentation
$description = preg_replace('/^[\s\t]*/m', '', $docBlock);
// Remove block comment syntax
$description = preg_replace('/^\/\*\*\s*|^\s*\*\s|^\s*\*\/$/m', '', $description);
// Remove annotations
$description = preg_replace('/^@.*$/m', '', $description);
// Ignore docs after a "--" separator
if (preg_match('/^--.*$/m', $description)) {
$descriptionParts = preg_split('/^--.*$/m', $description);
$description = array_shift($descriptionParts);
}
// Trim leading and trailing newlines
$description = trim($description, "\r\n");
return $description;
}
/**
* Checks if provided doc lien is empty.
*
* @param string $docLine
*
* @return Boolean
*/
private function isEmpty($docLine)
{
return '' == $docLine;
}
/**
* Checks if provided doc line is not an annotation.
*
* @param string $docLine
*
* @return Boolean
*/
private function isNotAnnotation($docLine)
{
return '@' !== substr($docLine, 0, 1);
}
/**
* Reads callee from provided doc line using registered annotation readers.
*
* @param string $class
* @param ReflectionMethod $method
* @param string $docLine
* @param null|string $description
*
* @return null|Callee
*/
private function readDocLineCallee($class, ReflectionMethod $method, $docLine, $description = null)
{
if ($this->isIgnoredAnnotation($docLine)) {
return null;
}
foreach ($this->readers as $reader) {
if ($callee = $reader->readCallee($class, $method, $docLine, $description)) {
return $callee;
}
}
return null;
}
/**
* Checks if provided doc line is one of the ignored annotations.
*
* @param string $docLine
*
* @return Boolean
*/
private function isIgnoredAnnotation($docLine)
{
$lowDocLine = strtolower($docLine);
foreach (self::$ignoreAnnotations as $ignoredAnnotation) {
if ($ignoredAnnotation == substr($lowDocLine, 0, strlen($ignoredAnnotation))) {
return true;
}
}
return false;
}
}

View file

@ -0,0 +1,32 @@
<?php
/*
* This file is part of the Behat.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Behat\Behat\Context\Reader;
use Behat\Behat\Context\Environment\ContextEnvironment;
use Behat\Testwork\Call\Callee;
/**
* Reads callees from a context class.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
interface ContextReader
{
/**
* Reads callees from specific environment & context.
*
* @param ContextEnvironment $environment
* @param string $contextClass
*
* @return Callee[]
*/
public function readContextCallees(ContextEnvironment $environment, $contextClass);
}

View file

@ -0,0 +1,54 @@
<?php
/*
* This file is part of the Behat.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Behat\Behat\Context\Reader;
use Behat\Behat\Context\Environment\ContextEnvironment;
/**
* Proxies call to another reader and caches context callees for a length of an entire exercise.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
final class ContextReaderCachedPerContext implements ContextReader
{
/**
* @var ContextReader
*/
private $childReader;
/**
* @var array[]
*/
private $cachedCallees = array();
/**
* Initializes reader.
*
* @param ContextReader $childReader
*/
public function __construct(ContextReader $childReader)
{
$this->childReader = $childReader;
}
/**
* {@inheritdoc}
*/
public function readContextCallees(ContextEnvironment $environment, $contextClass)
{
if (isset($this->cachedCallees[$contextClass])) {
return $this->cachedCallees[$contextClass];
}
return $this->cachedCallees[$contextClass] = $this->childReader->readContextCallees(
$environment, $contextClass
);
}
}

View file

@ -0,0 +1,69 @@
<?php
/*
* This file is part of the Behat.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Behat\Behat\Context\Reader;
use Behat\Behat\Context\Environment\ContextEnvironment;
/**
* Proxies call to another reader and caches callees for a length of an entire suite.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
final class ContextReaderCachedPerSuite implements ContextReader
{
/**
* @var ContextReader
*/
private $childReader;
/**
* @var array[]
*/
private $cachedCallees = array();
/**
* Initializes reader.
*
* @param ContextReader $childReader
*/
public function __construct(ContextReader $childReader)
{
$this->childReader = $childReader;
}
/**
* {@inheritdoc}
*/
public function readContextCallees(ContextEnvironment $environment, $contextClass)
{
$key = $this->generateCacheKey($environment, $contextClass);
if (isset($this->cachedCallees[$key])) {
return $this->cachedCallees[$key];
}
return $this->cachedCallees[$key] = $this->childReader->readContextCallees(
$environment, $contextClass
);
}
/**
* Generates cache key.
*
* @param ContextEnvironment $environment
* @param string $contextClass
*
* @return string
*/
private function generateCacheKey(ContextEnvironment $environment, $contextClass)
{
return $environment->getSuite()->getName() . $contextClass;
}
}

View file

@ -0,0 +1,101 @@
<?php
/*
* This file is part of the Behat.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Behat\Behat\Context\Reader;
use Behat\Behat\Context\Environment\ContextEnvironment;
use Behat\Behat\Context\Exception\UnknownTranslationResourceException;
use Behat\Behat\Context\TranslatableContext;
use Symfony\Component\Translation\Translator;
/**
* Reads translation resources from translatable contexts.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
final class TranslatableContextReader implements ContextReader
{
/**
* @var Translator
*/
private $translator;
/**
* Initializes loader.
*
* @param Translator $translator
*/
public function __construct(Translator $translator)
{
$this->translator = $translator;
}
/**
* {@inheritdoc}
*
* @see TranslatableContext
*/
public function readContextCallees(ContextEnvironment $environment, $contextClass)
{
$reflClass = new \ReflectionClass($contextClass);
if (!$reflClass->implementsInterface('Behat\Behat\Context\TranslatableContext')) {
return array();
}
$assetsId = $environment->getSuite()->getName();
foreach (call_user_func(array($contextClass, 'getTranslationResources')) as $path) {
$this->addTranslationResource($path, $assetsId);
}
return array();
}
/**
* Adds translation resource.
*
* @param string $path
* @param string $assetsId
*
* @throws UnknownTranslationResourceException
*/
private function addTranslationResource($path, $assetsId)
{
switch ($ext = pathinfo($path, PATHINFO_EXTENSION)) {
case 'yml':
$this->addTranslatorResource('yaml', $path, basename($path, '.' . $ext), $assetsId);
break;
case 'xliff':
$this->addTranslatorResource('xliff', $path, basename($path, '.' . $ext), $assetsId);
break;
case 'php':
$this->addTranslatorResource('php', $path, basename($path, '.' . $ext), $assetsId);
break;
default:
throw new UnknownTranslationResourceException(sprintf(
'Can not read translations from `%s`. File type is not supported.',
$path
), $path);
}
}
/**
* Adds resource to translator instance.
*
* @param string $type
* @param string $path
* @param string $language
* @param string $assetsId
*/
private function addTranslatorResource($type, $path, $language, $assetsId)
{
$this->translator->addResource($type, $path, $language, $assetsId);
}
}

View file

@ -0,0 +1,416 @@
<?php
/*
* This file is part of the Behat.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Behat\Behat\Context\ServiceContainer;
use Behat\Behat\Definition\ServiceContainer\DefinitionExtension;
use Behat\Behat\Snippet\ServiceContainer\SnippetExtension;
use Behat\Testwork\Argument\ServiceContainer\ArgumentExtension;
use Behat\Testwork\Autoloader\ServiceContainer\AutoloaderExtension;
use Behat\Testwork\Cli\ServiceContainer\CliExtension;
use Behat\Testwork\Environment\ServiceContainer\EnvironmentExtension;
use Behat\Testwork\Filesystem\ServiceContainer\FilesystemExtension;
use Behat\Testwork\ServiceContainer\Extension;
use Behat\Testwork\ServiceContainer\ExtensionManager;
use Behat\Testwork\ServiceContainer\ServiceProcessor;
use Behat\Testwork\Suite\ServiceContainer\SuiteExtension;
use Behat\Testwork\Translator\ServiceContainer\TranslatorExtension;
use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference;
/**
* Behat context extension.
*
* Extends Behat with context services.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
final class ContextExtension implements Extension
{
/**
* Available services
*/
const FACTORY_ID = 'context.factory';
const CONTEXT_SNIPPET_GENERATOR_ID = 'snippet.generator.context';
const AGGREGATE_RESOLVER_FACTORY_ID = 'context.argument.aggregate_resolver_factory';
/*
* Available extension points
*/
const CLASS_RESOLVER_TAG = 'context.class_resolver';
const ARGUMENT_RESOLVER_TAG = 'context.argument_resolver';
const INITIALIZER_TAG = 'context.initializer';
const READER_TAG = 'context.reader';
const ANNOTATION_READER_TAG = 'context.annotation_reader';
const CLASS_GENERATOR_TAG = 'context.class_generator';
const SUITE_SCOPED_RESOLVER_FACTORY_TAG = 'context.argument.suite_resolver_factory';
/**
* @var ServiceProcessor
*/
private $processor;
/**
* Initializes compiler pass.
*
* @param null|ServiceProcessor $processor
*/
public function __construct(ServiceProcessor $processor = null)
{
$this->processor = $processor ? : new ServiceProcessor();
}
/**
* {@inheritdoc}
*/
public function getConfigKey()
{
return 'contexts';
}
/**
* {@inheritdoc}
*/
public function initialize(ExtensionManager $extensionManager)
{
}
/**
* {@inheritdoc}
*/
public function configure(ArrayNodeDefinition $builder)
{
}
/**
* {@inheritdoc}
*/
public function load(ContainerBuilder $container, array $config)
{
$this->loadFactory($container);
$this->loadArgumentResolverFactory($container);
$this->loadEnvironmentHandler($container);
$this->loadEnvironmentReader($container);
$this->loadSuiteSetup($container);
$this->loadSnippetAppender($container);
$this->loadSnippetGenerators($container);
$this->loadSnippetsController($container);
$this->loadDefaultClassGenerators($container);
$this->loadDefaultContextReaders($container);
}
/**
* {@inheritdoc}
*/
public function process(ContainerBuilder $container)
{
$this->processClassResolvers($container);
$this->processArgumentResolverFactories($container);
$this->processArgumentResolvers($container);
$this->processContextInitializers($container);
$this->processContextReaders($container);
$this->processClassGenerators($container);
$this->processAnnotationReaders($container);
}
/**
* Loads context factory.
*
* @param ContainerBuilder $container
*/
private function loadFactory(ContainerBuilder $container)
{
$definition = new Definition('Behat\Behat\Context\ContextFactory', array(
new Reference(ArgumentExtension::CONSTRUCTOR_ARGUMENT_ORGANISER_ID)
));
$container->setDefinition(self::FACTORY_ID, $definition);
}
/**
* Loads argument resolver factory used in the environment handler.
*
* @param ContainerBuilder $container
*/
private function loadArgumentResolverFactory(ContainerBuilder $container)
{
$definition = new Definition('Behat\Behat\Context\Argument\CompositeFactory');
$container->setDefinition(self::AGGREGATE_RESOLVER_FACTORY_ID, $definition);
}
/**
* Loads context environment handlers.
*
* @param ContainerBuilder $container
*/
private function loadEnvironmentHandler(ContainerBuilder $container)
{
$definition = new Definition('Behat\Behat\Context\Environment\Handler\ContextEnvironmentHandler', array(
new Reference(self::FACTORY_ID),
new Reference(self::AGGREGATE_RESOLVER_FACTORY_ID)
));
$definition->addTag(EnvironmentExtension::HANDLER_TAG, array('priority' => 50));
$container->setDefinition(self::getEnvironmentHandlerId(), $definition);
}
/**
* Loads context environment readers.
*
* @param ContainerBuilder $container
*/
private function loadEnvironmentReader(ContainerBuilder $container)
{
$definition = new Definition('Behat\Behat\Context\Environment\Reader\ContextEnvironmentReader');
$definition->addTag(EnvironmentExtension::READER_TAG, array('priority' => 50));
$container->setDefinition(self::getEnvironmentReaderId(), $definition);
}
/**
* Loads context environment setup.
*
* @param ContainerBuilder $container
*/
private function loadSuiteSetup(ContainerBuilder $container)
{
$definition = new Definition('Behat\Behat\Context\Suite\Setup\SuiteWithContextsSetup', array(
new Reference(AutoloaderExtension::CLASS_LOADER_ID),
new Reference(FilesystemExtension::LOGGER_ID)
));
$definition->addTag(SuiteExtension::SETUP_TAG, array('priority' => 20));
$container->setDefinition(self::getSuiteSetupId(), $definition);
}
/**
* Loads context snippet appender.
*
* @param ContainerBuilder $container
*/
private function loadSnippetAppender(ContainerBuilder $container)
{
$definition = new Definition('Behat\Behat\Context\Snippet\Appender\ContextSnippetAppender', array(
new Reference(FilesystemExtension::LOGGER_ID)
));
$definition->addTag(SnippetExtension::APPENDER_TAG, array('priority' => 50));
$container->setDefinition(SnippetExtension::APPENDER_TAG . '.context', $definition);
}
/**
* Loads context snippet generators.
*
* @param ContainerBuilder $container
*/
private function loadSnippetGenerators(ContainerBuilder $container)
{
$definition = new Definition('Behat\Behat\Context\Snippet\Generator\ContextSnippetGenerator', array(
new Reference(DefinitionExtension::PATTERN_TRANSFORMER_ID)
));
$definition->addTag(SnippetExtension::GENERATOR_TAG, array('priority' => 50));
$container->setDefinition(self::CONTEXT_SNIPPET_GENERATOR_ID, $definition);
}
/**
* @param ContainerBuilder $container
*/
protected function loadSnippetsController(ContainerBuilder $container)
{
$definition = new Definition('Behat\Behat\Context\Cli\ContextSnippetsController', array(
new Reference(self::CONTEXT_SNIPPET_GENERATOR_ID),
new Reference(TranslatorExtension::TRANSLATOR_ID)
));
$definition->addTag(CliExtension::CONTROLLER_TAG, array('priority' => 410));
$container->setDefinition(CliExtension::CONTROLLER_TAG . '.context_snippets', $definition);
}
/**
* Loads default context class generators.
*
* @param ContainerBuilder $container
*/
private function loadDefaultClassGenerators(ContainerBuilder $container)
{
$definition = new Definition('Behat\Behat\Context\ContextClass\SimpleClassGenerator');
$definition->addTag(self::CLASS_GENERATOR_TAG, array('priority' => 50));
$container->setDefinition(self::CLASS_GENERATOR_TAG . '.simple', $definition);
}
/**
* Loads default context readers.
*
* @param ContainerBuilder $container
*/
private function loadDefaultContextReaders(ContainerBuilder $container)
{
$definition = new Definition('Behat\Behat\Context\Reader\AnnotatedContextReader');
$container->setDefinition(self::getAnnotatedContextReaderId(), $definition);
$definition = new Definition('Behat\Behat\Context\Reader\ContextReaderCachedPerContext', array(
new Reference(self::getAnnotatedContextReaderId())
));
$definition->addTag(self::READER_TAG, array('priority' => 50));
$container->setDefinition(self::getAnnotatedContextReaderId() . '.cached', $definition);
$definition = new Definition('Behat\Behat\Context\Reader\TranslatableContextReader', array(
new Reference(TranslatorExtension::TRANSLATOR_ID)
));
$container->setDefinition(self::READER_TAG . '.translatable', $definition);
$definition = new Definition('Behat\Behat\Context\Reader\ContextReaderCachedPerSuite', array(
new Reference(self::READER_TAG . '.translatable')
));
$definition->addTag(self::READER_TAG, array('priority' => 50));
$container->setDefinition(self::READER_TAG . '.translatable.cached', $definition);
}
/**
* Processes all class resolvers.
*
* @param ContainerBuilder $container
*/
private function processClassResolvers(ContainerBuilder $container)
{
$references = $this->processor->findAndSortTaggedServices($container, self::CLASS_RESOLVER_TAG);
$definition = $container->getDefinition(self::getEnvironmentHandlerId());
foreach ($references as $reference) {
$definition->addMethodCall('registerClassResolver', array($reference));
}
}
/**
* Processes all argument resolver factories.
*
* @param ContainerBuilder $container
*/
private function processArgumentResolverFactories($container)
{
$references = $this->processor->findAndSortTaggedServices($container, self::SUITE_SCOPED_RESOLVER_FACTORY_TAG);
$definition = $container->getDefinition(self::AGGREGATE_RESOLVER_FACTORY_ID);
foreach ($references as $reference) {
$definition->addMethodCall('registerFactory', array($reference));
}
}
/**
* Processes all argument resolvers.
*
* @param ContainerBuilder $container
*/
private function processArgumentResolvers(ContainerBuilder $container)
{
$references = $this->processor->findAndSortTaggedServices($container, self::ARGUMENT_RESOLVER_TAG);
$definition = $container->getDefinition(self::FACTORY_ID);
foreach ($references as $reference) {
$definition->addMethodCall('registerArgumentResolver', array($reference));
}
}
/**
* Processes all context initializers.
*
* @param ContainerBuilder $container
*/
private function processContextInitializers(ContainerBuilder $container)
{
$references = $this->processor->findAndSortTaggedServices($container, self::INITIALIZER_TAG);
$definition = $container->getDefinition(self::FACTORY_ID);
foreach ($references as $reference) {
$definition->addMethodCall('registerContextInitializer', array($reference));
}
}
/**
* Processes all context readers.
*
* @param ContainerBuilder $container
*/
private function processContextReaders(ContainerBuilder $container)
{
$references = $this->processor->findAndSortTaggedServices($container, self::READER_TAG);
$definition = $container->getDefinition(self::getEnvironmentReaderId());
foreach ($references as $reference) {
$definition->addMethodCall('registerContextReader', array($reference));
}
}
/**
* Processes all class generators.
*
* @param ContainerBuilder $container
*/
private function processClassGenerators(ContainerBuilder $container)
{
$references = $this->processor->findAndSortTaggedServices($container, self::CLASS_GENERATOR_TAG);
$definition = $container->getDefinition(self::getSuiteSetupId());
foreach ($references as $reference) {
$definition->addMethodCall('registerClassGenerator', array($reference));
}
}
/**
* Processes all annotation readers.
*
* @param ContainerBuilder $container
*/
private function processAnnotationReaders(ContainerBuilder $container)
{
$references = $this->processor->findAndSortTaggedServices($container, self::ANNOTATION_READER_TAG);
$definition = $container->getDefinition(self::getAnnotatedContextReaderId());
foreach ($references as $reference) {
$definition->addMethodCall('registerAnnotationReader', array($reference));
}
}
/**
* Returns context environment handler service id.
*
* @return string
*/
private static function getEnvironmentHandlerId()
{
return EnvironmentExtension::HANDLER_TAG . '.context';
}
/**
* Returns context environment reader id.
*
* @return string
*/
private static function getEnvironmentReaderId()
{
return EnvironmentExtension::READER_TAG . '.context';
}
/**
* Returns context suite setup id.
*
* @return string
*/
private static function getSuiteSetupId()
{
return SuiteExtension::SETUP_TAG . '.suite_with_contexts';
}
/**
* Returns annotated context reader id.
*
* @return string
*/
private static function getAnnotatedContextReaderId()
{
return self::READER_TAG . '.annotated';
}
}

View file

@ -0,0 +1,128 @@
<?php
/*
* This file is part of the Behat.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Behat\Behat\Context\Snippet\Appender;
use Behat\Behat\Snippet\AggregateSnippet;
use Behat\Behat\Snippet\Appender\SnippetAppender;
use Behat\Testwork\Filesystem\FilesystemLogger;
use ReflectionClass;
/**
* Appends context-related snippets to their context classes.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
final class ContextSnippetAppender implements SnippetAppender
{
/**
* @const PendingException class
*/
const PENDING_EXCEPTION_CLASS = 'Behat\Behat\Tester\Exception\PendingException';
/**
* @var FilesystemLogger
*/
private $logger;
/**
* Initializes appender.
*
* @param null|FilesystemLogger $logger
*/
public function __construct(FilesystemLogger $logger = null)
{
$this->logger = $logger;
}
/**
* {@inheritdoc}
*/
public function supportsSnippet(AggregateSnippet $snippet)
{
return 'context' === $snippet->getType();
}
/**
* {@inheritdoc}
*/
public function appendSnippet(AggregateSnippet $snippet)
{
foreach ($snippet->getTargets() as $contextClass) {
$reflection = new ReflectionClass($contextClass);
$content = file_get_contents($reflection->getFileName());
foreach ($snippet->getUsedClasses() as $class) {
if (!$this->isClassImported($class, $content)) {
$content = $this->importClass($class, $content);
}
}
$generated = rtrim(strtr($snippet->getSnippet(), array('\\' => '\\\\', '$' => '\\$')));
$content = preg_replace('/}\s*$/', "\n" . $generated . "\n}\n", $content);
$path = $reflection->getFileName();
file_put_contents($path, $content);
$this->logSnippetAddition($snippet, $path);
}
}
/**
* Checks if context file already has class in it.
*
* @param string $class
* @param string $contextFileContent
*
* @return Boolean
*/
private function isClassImported($class, $contextFileContent)
{
$classImportRegex = sprintf(
'@use[^;]*%s.*;@ms',
preg_quote($class, '@')
);
return 1 === preg_match($classImportRegex, $contextFileContent);
}
/**
* Adds use-block for class.
*
* @param string $class
* @param string $contextFileContent
*
* @return string
*/
private function importClass($class, $contextFileContent)
{
$replaceWith = "\$1" . 'use ' . $class . ";\n\$2;";
return preg_replace('@^(.*)(use\s+[^;]*);@m', $replaceWith, $contextFileContent, 1);
}
/**
* Logs snippet addition to the provided path (if logger is given).
*
* @param AggregateSnippet $snippet
* @param string $path
*/
private function logSnippetAddition(AggregateSnippet $snippet, $path)
{
if (!$this->logger) {
return;
}
$steps = $snippet->getSteps();
$reason = sprintf("`<comment>%s</comment>` definition added", $steps[0]->getText());
$this->logger->fileUpdated($path, $reason);
}
}

View file

@ -0,0 +1,105 @@
<?php
/*
* This file is part of the Behat.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Behat\Behat\Context\Snippet;
use Behat\Behat\Snippet\Snippet;
use Behat\Gherkin\Node\StepNode;
/**
* Represents a definition snippet for a context class.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
final class ContextSnippet implements Snippet
{
/**
* @var StepNode
*/
private $step;
/**
* @var string
*/
private $template;
/**
* @var string
*/
private $contextClass;
/**
* @var string[]
*/
private $usedClasses;
/**
* Initializes definition snippet.
*
* @param StepNode $step
* @param string $template
* @param string $contextClass
* @param string[] $usedClasses
*/
public function __construct(StepNode $step, $template, $contextClass, array $usedClasses = array())
{
$this->step = $step;
$this->template = $template;
$this->contextClass = $contextClass;
$this->usedClasses = $usedClasses;
}
/**
* {@inheritdoc}
*/
public function getType()
{
return 'context';
}
/**
* {@inheritdoc}
*/
public function getHash()
{
return md5($this->template);
}
/**
* {@inheritdoc}
*/
public function getSnippet()
{
return sprintf($this->template, $this->step->getKeywordType());
}
/**
* {@inheritdoc}
*/
public function getStep()
{
return $this->step;
}
/**
* {@inheritdoc}
*/
public function getTarget()
{
return $this->contextClass;
}
/**
* Returns the classes used in the snippet which should be imported.
*
* @return string[]
*/
public function getUsedClasses()
{
return $this->usedClasses;
}
}

View file

@ -0,0 +1,56 @@
<?php
/*
* This file is part of the Behat.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Behat\Behat\Context\Snippet\Generator;
use Behat\Behat\Context\Environment\ContextEnvironment;
/**
* Uses multiple child identifiers - the first one that returns non-null result would
* be the winner.
*
* This behaviour was introduced in 3.x to support the BC for interface-focused
* context identifier, while providing better user experience (no need to explicitly
* call `--snippets-for` on `--append-snippets` when contexts do not implement any
* snippet accepting interfaces).
*/
final class AggregateContextIdentifier implements TargetContextIdentifier
{
/**
* @var TargetContextIdentifier[]
*/
private $identifiers;
/**
* Initialises identifier.
*
* @param TargetContextIdentifier[] $identifiers
*/
public function __construct(array $identifiers)
{
$this->identifiers = $identifiers;
}
/**
* {@inheritdoc}
*/
public function guessTargetContextClass(ContextEnvironment $environment)
{
foreach ($this->identifiers as $identifier) {
$contextClass = $identifier->guessTargetContextClass($environment);
if (null !== $contextClass) {
return $contextClass;
}
}
return null;
}
}

View file

@ -0,0 +1,49 @@
<?php
/*
* This file is part of the Behat.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Behat\Behat\Context\Snippet\Generator;
/**
* Uses multiple child identifiers - the first one that returns non-null result would
* be the winner.
*/
final class AggregatePatternIdentifier implements PatternIdentifier
{
/**
* @var PatternIdentifier[]
*/
private $identifiers;
/**
* Initialises identifier.
*
* @param PatternIdentifier[] $identifiers
*/
public function __construct(array $identifiers)
{
$this->identifiers = $identifiers;
}
/**
* {@inheritdoc}
*/
public function guessPatternType($contextClass)
{
foreach ($this->identifiers as $identifier) {
$pattern = $identifier->guessPatternType($contextClass);
if (null !== $pattern) {
return $pattern;
}
}
return null;
}
}

View file

@ -0,0 +1,54 @@
<?php
/*
* This file is part of the Behat.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Behat\Behat\Context\Snippet\Generator;
use Behat\Behat\Context\Environment\ContextEnvironment;
/**
* Decorates actual identifier and caches its answers per suite.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
final class CachedContextIdentifier implements TargetContextIdentifier
{
/**
* @var TargetContextIdentifier
*/
private $decoratedIdentifier;
/**
* @var array
*/
private $contextClasses = array();
/**
* Initialise the identifier.
*
* @param TargetContextIdentifier $identifier
*/
public function __construct(TargetContextIdentifier $identifier)
{
$this->decoratedIdentifier = $identifier;
}
/**
* {@inheritdoc}
*/
public function guessTargetContextClass(ContextEnvironment $environment)
{
$suiteKey = $environment->getSuite()->getName();
if (array_key_exists($suiteKey, $this->contextClasses)) {
return $this->contextClasses[$suiteKey];
}
return $this->contextClasses[$suiteKey] = $this->decoratedIdentifier->guessTargetContextClass($environment);
}
}

View file

@ -0,0 +1,37 @@
<?php
/*
* This file is part of the Behat.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Behat\Behat\Context\Snippet\Generator;
use Behat\Behat\Context\Environment\ContextEnvironment;
/**
* Identifier that uses context interfaces to guess which one is target.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*
* @deprecated in favour of --snippets-for and will be removed in 4.0
*/
final class ContextInterfaceBasedContextIdentifier implements TargetContextIdentifier
{
/**
* {@inheritdoc}
*/
public function guessTargetContextClass(ContextEnvironment $environment)
{
foreach ($environment->getContextClasses() as $class) {
if (in_array('Behat\Behat\Context\SnippetAcceptingContext', class_implements($class))) {
return $class;
}
}
return null;
}
}

View file

@ -0,0 +1,33 @@
<?php
/*
* This file is part of the Behat.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Behat\Behat\Context\Snippet\Generator;
/**
* Identifier that uses context interfaces to guess the pattern type.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*
* @deprecated in favour of --snippet-type and will be removed in 4.0
*/
final class ContextInterfaceBasedPatternIdentifier implements PatternIdentifier
{
/**
* {@inheritdoc}
*/
public function guessPatternType($contextClass)
{
if (!in_array('Behat\Behat\Context\CustomSnippetAcceptingContext', class_implements($contextClass))) {
return null;
}
return $contextClass::getAcceptedSnippetType();
}
}

View file

@ -0,0 +1,360 @@
<?php
/*
* This file is part of the Behat.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Behat\Behat\Context\Snippet\Generator;
use Behat\Behat\Context\Environment\ContextEnvironment;
use Behat\Behat\Context\Snippet\ContextSnippet;
use Behat\Behat\Definition\Pattern\PatternTransformer;
use Behat\Behat\Snippet\Exception\EnvironmentSnippetGenerationException;
use Behat\Behat\Snippet\Generator\SnippetGenerator;
use Behat\Gherkin\Node\PyStringNode;
use Behat\Gherkin\Node\StepNode;
use Behat\Gherkin\Node\TableNode;
use Behat\Testwork\Environment\Environment;
use ReflectionClass;
/**
* Generates snippets for a context class.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
final class ContextSnippetGenerator implements SnippetGenerator
{
/**
* @var string[string]
*/
private static $proposedMethods = array();
/**
* @var string
*/
private static $templateTemplate = <<<TPL
/**
* @%%s %s
*/
public function %s(%s)
{
throw new PendingException();
}
TPL;
/**
* @var PatternTransformer
*/
private $patternTransformer;
/**
* @var TargetContextIdentifier
*/
private $contextIdentifier;
/**
* @var PatternIdentifier
*/
private $patternIdentifier;
/**
* Initializes snippet generator.
*
* @param PatternTransformer $patternTransformer
*/
public function __construct(PatternTransformer $patternTransformer)
{
$this->patternTransformer = $patternTransformer;
$this->setContextIdentifier(new FixedContextIdentifier(null));
$this->setPatternIdentifier(new FixedPatternIdentifier(null));
}
/**
* Sets target context identifier.
*
* @param TargetContextIdentifier $identifier
*/
public function setContextIdentifier(TargetContextIdentifier $identifier)
{
$this->contextIdentifier = new CachedContextIdentifier($identifier);
}
/**
* Sets target pattern type identifier.
*
* @param PatternIdentifier $identifier
*/
public function setPatternIdentifier(PatternIdentifier $identifier)
{
$this->patternIdentifier = $identifier;
}
/**
* {@inheritdoc}
*/
public function supportsEnvironmentAndStep(Environment $environment, StepNode $step)
{
if (!$environment instanceof ContextEnvironment) {
return false;
}
if (!$environment->hasContexts()) {
return false;
}
return null !== $this->contextIdentifier->guessTargetContextClass($environment);
}
/**
* {@inheritdoc}
*/
public function generateSnippet(Environment $environment, StepNode $step)
{
if (!$environment instanceof ContextEnvironment) {
throw new EnvironmentSnippetGenerationException(sprintf(
'ContextSnippetGenerator does not support `%s` environment.',
get_class($environment)
), $environment);
}
$contextClass = $this->contextIdentifier->guessTargetContextClass($environment);
$patternType = $this->patternIdentifier->guessPatternType($contextClass);
$stepText = $step->getText();
$pattern = $this->patternTransformer->generatePattern($patternType, $stepText);
$methodName = $this->getMethodName($contextClass, $pattern->getCanonicalText(), $pattern->getPattern());
$methodArguments = $this->getMethodArguments($step, $pattern->getPlaceholderCount());
$snippetTemplate = $this->getSnippetTemplate($pattern->getPattern(), $methodName, $methodArguments);
$usedClasses = $this->getUsedClasses($step);
return new ContextSnippet($step, $snippetTemplate, $contextClass, $usedClasses);
}
/**
* Generates method name using step text and regex.
*
* @param string $contextClass
* @param string $canonicalText
* @param string $pattern
*
* @return string
*/
private function getMethodName($contextClass, $canonicalText, $pattern)
{
$methodName = $this->deduceMethodName($canonicalText);
$methodName = $this->getUniqueMethodName($contextClass, $pattern, $methodName);
return $methodName;
}
/**
* Returns an array of method argument names from step and token count.
*
* @param StepNode $step
* @param integer $tokenCount
*
* @return string[]
*/
private function getMethodArguments(StepNode $step, $tokenCount)
{
$args = array();
for ($i = 0; $i < $tokenCount; $i++) {
$args[] = '$arg' . ($i + 1);
}
foreach ($step->getArguments() as $argument) {
$args[] = $this->getMethodArgument($argument);
}
return $args;
}
/**
* Returns an array of classes used by the snippet template
*
* @param StepNode $step
*
* @return string[]
*/
private function getUsedClasses(StepNode $step)
{
$usedClasses = array('Behat\Behat\Tester\Exception\PendingException');
foreach ($step->getArguments() as $argument) {
if ($argument instanceof TableNode) {
$usedClasses[] = 'Behat\Gherkin\Node\TableNode';
} elseif ($argument instanceof PyStringNode) {
$usedClasses[] = 'Behat\Gherkin\Node\PyStringNode';
}
}
return $usedClasses;
}
/**
* Generates snippet template using regex, method name and arguments.
*
* @param string $pattern
* @param string $methodName
* @param string[] $methodArguments
*
* @return string
*/
private function getSnippetTemplate($pattern, $methodName, array $methodArguments)
{
return sprintf(
self::$templateTemplate,
str_replace('%', '%%', $pattern),
$methodName,
implode(', ', $methodArguments)
);
}
/**
* Generates definition method name based on the step text.
*
* @param string $canonicalText
*
* @return string
*/
private function deduceMethodName($canonicalText)
{
// check that method name is not empty
if (0 !== strlen($canonicalText)) {
$canonicalText[0] = strtolower($canonicalText[0]);
return $canonicalText;
}
return 'stepDefinition1';
}
/**
* Ensures uniqueness of the method name in the context.
*
* @param string $contextClass
* @param string $stepPattern
* @param string $name
*
* @return string
*/
private function getUniqueMethodName($contextClass, $stepPattern, $name)
{
$reflection = new ReflectionClass($contextClass);
$number = $this->getMethodNumberFromTheMethodName($name);
list($name, $number) = $this->getMethodNameNotExistentInContext($reflection, $name, $number);
$name = $this->getMethodNameNotProposedEarlier($contextClass, $stepPattern, $name, $number);
return $name;
}
/**
* Tries to deduct method number from the provided method name.
*
* @param string $methodName
*
* @return integer
*/
private function getMethodNumberFromTheMethodName($methodName)
{
$methodNumber = 2;
if (preg_match('/(\d+)$/', $methodName, $matches)) {
$methodNumber = intval($matches[1]);
}
return $methodNumber;
}
/**
* Tries to guess method name that is not yet defined in the context class.
*
* @param ReflectionClass $reflection
* @param string $methodName
* @param integer $methodNumber
*
* @return array
*/
private function getMethodNameNotExistentInContext(ReflectionClass $reflection, $methodName, $methodNumber)
{
while ($reflection->hasMethod($methodName)) {
$methodName = preg_replace('/\d+$/', '', $methodName);
$methodName .= $methodNumber++;
}
return array($methodName, $methodNumber);
}
/**
* Tries to guess method name that is not yet proposed to the context class.
*
* @param string $contextClass
* @param string $stepPattern
* @param string $name
* @param integer $number
*
* @return string
*/
private function getMethodNameNotProposedEarlier($contextClass, $stepPattern, $name, $number)
{
foreach ($this->getAlreadyProposedMethods($contextClass) as $proposedPattern => $proposedMethod) {
if ($proposedPattern === $stepPattern) {
continue;
}
while ($proposedMethod === $name) {
$name = preg_replace('/\d+$/', '', $name);
$name .= $number++;
}
}
$this->markMethodAsAlreadyProposed($contextClass, $stepPattern, $name);
return $name;
}
/**
* Returns already proposed method names.
*
* @param string $contextClass
*
* @return string[]
*/
private function getAlreadyProposedMethods($contextClass)
{
return isset(self::$proposedMethods[$contextClass]) ? self::$proposedMethods[$contextClass] : array();
}
/**
* Marks method as proposed one.
*
* @param string $contextClass
* @param string $stepPattern
* @param string $methodName
*/
private function markMethodAsAlreadyProposed($contextClass, $stepPattern, $methodName)
{
self::$proposedMethods[$contextClass][$stepPattern] = $methodName;
}
/**
* Returns method argument.
*
* @param string $argument
*
* @return string
*/
private function getMethodArgument($argument)
{
$arg = '__unknown__';
if ($argument instanceof PyStringNode) {
$arg = 'PyStringNode $string';
} elseif ($argument instanceof TableNode) {
$arg = 'TableNode $table';
}
return $arg;
}
}

View file

@ -0,0 +1,48 @@
<?php
/*
* This file is part of the Behat.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Behat\Behat\Context\Snippet\Generator;
use Behat\Behat\Context\Environment\ContextEnvironment;
/**
* Identifier that always returns same context, if it is defined in the suite.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
final class FixedContextIdentifier implements TargetContextIdentifier
{
/**
* @var
*/
private $contextClass;
/**
* Initialises identifier.
*
* @param string $contextClass
*/
public function __construct($contextClass)
{
$this->contextClass = $contextClass;
}
/**
* {@inheritdoc}
*/
public function guessTargetContextClass(ContextEnvironment $environment)
{
if ($environment->hasContextClass($this->contextClass)) {
return $this->contextClass;
}
return null;
}
}

View file

@ -0,0 +1,44 @@
<?php
/*
* This file is part of the Behat.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Behat\Behat\Context\Snippet\Generator;
use Behat\Behat\Context\Context;
/**
* Identifier that always returns same pattern type.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
final class FixedPatternIdentifier implements PatternIdentifier
{
/**
* @var string
*/
private $patternType;
/**
* Initialises identifier.
*
* @param string $patternType
*/
public function __construct($patternType)
{
$this->patternType = $patternType;
}
/**
* {@inheritdoc}
*/
public function guessPatternType($contextClass)
{
return $this->patternType;
}
}

View file

@ -0,0 +1,30 @@
<?php
/*
* This file is part of the Behat.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Behat\Behat\Context\Snippet\Generator;
use Behat\Behat\Context\Context;
/**
* Identifies target pattern for snippets.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
interface PatternIdentifier
{
/**
* Attempts to guess the target pattern type from the context.
*
* @param string $contextClass
*
* @return null|string
*/
public function guessPatternType($contextClass);
}

View file

@ -0,0 +1,30 @@
<?php
/*
* This file is part of the Behat.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Behat\Behat\Context\Snippet\Generator;
use Behat\Behat\Context\Environment\ContextEnvironment;
/**
* Identifies target context for snippets.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
interface TargetContextIdentifier
{
/**
* Attempts to guess the target context class from the environment.
*
* @param ContextEnvironment $environment
*
* @return null|string
*/
public function guessTargetContextClass(ContextEnvironment $environment);
}

View file

@ -0,0 +1,26 @@
<?php
/*
* This file is part of the Behat.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Behat\Behat\Context;
use Behat\Behat\Context\Snippet\Generator\ContextSnippetGenerator;
/**
* Context that implements this interface is treated as a snippet-friendly context.
*
* @see ContextSnippetGenerator
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*
* @deprecated will be removed in 4.0. Use --snippets-for CLI option instead
*/
interface SnippetAcceptingContext extends Context
{
}

View file

@ -0,0 +1,246 @@
<?php
/*
* This file is part of the Behat.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Behat\Behat\Context\Suite\Setup;
use Behat\Behat\Context\ContextClass\ClassGenerator;
use Behat\Behat\Context\Exception\ContextNotFoundException;
use Behat\Testwork\Filesystem\FilesystemLogger;
use Behat\Testwork\Suite\Exception\SuiteConfigurationException;
use Behat\Testwork\Suite\Setup\SuiteSetup;
use Behat\Testwork\Suite\Suite;
use Symfony\Component\ClassLoader\ClassLoader;
/**
* Generates classes for all contexts in the suite using autoloader.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
final class SuiteWithContextsSetup implements SuiteSetup
{
/**
* @var ClassLoader
*/
private $autoloader;
/**
* @var null|FilesystemLogger
*/
private $logger;
/**
* @var ClassGenerator[]
*/
private $classGenerators = array();
/**
* Initializes setup.
*
* @param ClassLoader $autoloader
* @param null|FilesystemLogger $logger
*/
public function __construct(ClassLoader $autoloader, FilesystemLogger $logger = null)
{
$this->autoloader = $autoloader;
$this->logger = $logger;
}
/**
* Registers class generator.
*
* @param ClassGenerator $generator
*/
public function registerClassGenerator(ClassGenerator $generator)
{
$this->classGenerators[] = $generator;
}
/**
* {@inheritdoc}
*/
public function supportsSuite(Suite $suite)
{
return $suite->hasSetting('contexts');
}
/**
* {@inheritdoc}
*/
public function setupSuite(Suite $suite)
{
foreach ($this->getNormalizedContextClasses($suite) as $class) {
if (class_exists($class)) {
continue;
}
$this->ensureContextDirectory($path = $this->findClassFile($class));
if ($content = $this->generateClass($suite, $class)) {
$this->createContextFile($path, $content);
}
}
}
/**
* Returns normalized context classes.
*
* @param Suite $suite
*
* @return string[]
*/
private function getNormalizedContextClasses(Suite $suite)
{
return array_map(
function ($context) {
return is_array($context) ? current(array_keys($context)) : $context;
},
$this->getSuiteContexts($suite)
);
}
/**
* Returns array of context classes configured for the provided suite.
*
* @param Suite $suite
*
* @return string[]
*
* @throws SuiteConfigurationException If `contexts` setting is not an array
*/
private function getSuiteContexts(Suite $suite)
{
$contexts = $suite->getSetting('contexts');
if (!is_array($contexts)) {
throw new SuiteConfigurationException(
sprintf('`contexts` setting of the "%s" suite is expected to be an array, `%s` given.',
$suite->getName(),
gettype($contexts)
),
$suite->getName()
);
}
return $contexts;
}
/**
* Creates context directory in the filesystem.
*
* @param string $path
*/
private function createContextDirectory($path)
{
mkdir($path, 0777, true);
if ($this->logger) {
$this->logger->directoryCreated($path, 'place your context classes here');
}
}
/**
* Creates context class file in the filesystem.
*
* @param string $path
* @param string $content
*/
private function createContextFile($path, $content)
{
file_put_contents($path, $content);
if ($this->logger) {
$this->logger->fileCreated($path, 'place your definitions, transformations and hooks here');
}
}
/**
* Finds file to store a class.
*
* @param string $class
*
* @return string
*
* @throws ContextNotFoundException If class file could not be determined
*/
private function findClassFile($class)
{
list($classpath, $classname) = $this->findClasspathAndClass($class);
$classpath .= str_replace('_', DIRECTORY_SEPARATOR, $classname) . '.php';
foreach ($this->autoloader->getPrefixes() as $prefix => $dirs) {
if (0 === strpos($class, $prefix)) {
return current($dirs) . DIRECTORY_SEPARATOR . $classpath;
}
}
if ($dirs = $this->autoloader->getFallbackDirs()) {
return current($dirs) . DIRECTORY_SEPARATOR . $classpath;
}
throw new ContextNotFoundException(sprintf(
'Could not find where to put "%s" class. Have you configured autoloader properly?',
$class
), $class);
}
/**
* Generates class using registered class generators.
*
* @param Suite $suite
* @param string $class
*
* @return null|string
*/
private function generateClass(Suite $suite, $class)
{
$content = null;
foreach ($this->classGenerators as $generator) {
if ($generator->supportsSuiteAndClass($suite, $class)) {
$content = $generator->generateClass($suite, $class);
}
}
return $content;
}
/**
* Ensures that directory for a classpath exists.
*
* @param string $classpath
*/
private function ensureContextDirectory($classpath)
{
if (!is_dir(dirname($classpath))) {
$this->createContextDirectory(dirname($classpath));
}
}
/**
* Finds classpath and classname from class.
*
* @param string $class
*
* @return array
*/
private function findClasspathAndClass($class)
{
if (false !== $pos = strrpos($class, '\\')) {
// namespaced class name
$classpath = str_replace('\\', DIRECTORY_SEPARATOR, substr($class, 0, $pos)) . DIRECTORY_SEPARATOR;
$classname = substr($class, $pos + 1);
return array($classpath, $classname);
}
// PEAR-like class name
$classpath = null;
$classname = $class;
return array($classpath, $classname);
}
}

View file

@ -0,0 +1,36 @@
<?php
/*
* This file is part of the Behat.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Behat\Behat\Context;
use Behat\Behat\Context\Reader\TranslatableContextReader;
/**
* Context that implements this interface is also treated as a translation provider for all it's callees.
*
* @see TranslatableContextReader
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
interface TranslatableContext extends Context
{
/**
* Returns array of Translator-supported resource paths.
*
* For instance:
*
* * array(__DIR__.'/../'ru.yml)
* * array(__DIR__.'/../'en.xliff)
* * array(__DIR__.'/../'de.php)
*
* @return string[]
*/
public static function getTranslationResources();
}

View file

@ -0,0 +1,78 @@
<?php
/*
* This file is part of the Behat.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Behat\Behat\Definition\Call;
use Behat\Behat\Definition\Definition;
use Behat\Gherkin\Node\FeatureNode;
use Behat\Gherkin\Node\StepNode;
use Behat\Testwork\Environment\Call\EnvironmentCall;
use Behat\Testwork\Environment\Environment;
/**
* Enhances environment call with definition information.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
final class DefinitionCall extends EnvironmentCall
{
/**
* @var FeatureNode
*/
private $feature;
/**
* @var StepNode
*/
private $step;
/**
* Initializes definition call.
*
* @param Environment $environment
* @param FeatureNode $feature
* @param StepNode $step
* @param Definition $definition
* @param array $arguments
* @param null|integer $errorReportingLevel
*/
public function __construct(
Environment $environment,
FeatureNode $feature,
StepNode $step,
Definition $definition,
array $arguments,
$errorReportingLevel = null
) {
parent::__construct($environment, $definition, $arguments, $errorReportingLevel);
$this->feature = $feature;
$this->step = $step;
}
/**
* Returns step feature node.
*
* @return FeatureNode
*/
public function getFeature()
{
return $this->feature;
}
/**
* Returns definition step node.
*
* @return StepNode
*/
public function getStep()
{
return $this->step;
}
}

View file

@ -0,0 +1,31 @@
<?php
/*
* This file is part of the Behat.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Behat\Behat\Definition\Call;
/**
* Given steps definition.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
final class Given extends RuntimeDefinition
{
/**
* Initializes definition.
*
* @param string $pattern
* @param callable $callable
* @param null|string $description
*/
public function __construct($pattern, $callable, $description = null)
{
parent::__construct('Given', $pattern, $callable, $description);
}
}

View file

@ -0,0 +1,71 @@
<?php
/*
* This file is part of the Behat.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Behat\Behat\Definition\Call;
use Behat\Behat\Definition\Definition;
use Behat\Testwork\Call\RuntimeCallee;
/**
* Represents a step definition created and executed in the runtime.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
abstract class RuntimeDefinition extends RuntimeCallee implements Definition
{
/**
* @var string
*/
private $type;
/**
* @var string
*/
private $pattern;
/**
* Initializes definition.
*
* @param string $type
* @param string $pattern
* @param callable $callable
* @param null|string $description
*/
public function __construct($type, $pattern, $callable, $description = null)
{
$this->type = $type;
$this->pattern = $pattern;
parent::__construct($callable, $description);
}
/**
* {@inheritdoc}
*/
public function getType()
{
return $this->type;
}
/**
* {@inheritdoc}
*/
public function getPattern()
{
return $this->pattern;
}
/**
* {@inheritdoc}
*/
public function __toString()
{
return $this->getType() . ' ' . $this->getPattern();
}
}

View file

@ -0,0 +1,31 @@
<?php
/*
* This file is part of the Behat.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Behat\Behat\Definition\Call;
/**
* Then steps definition.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
final class Then extends RuntimeDefinition
{
/**
* Initializes definition.
*
* @param string $pattern
* @param callable $callable
* @param null|string $description
*/
public function __construct($pattern, $callable, $description = null)
{
parent::__construct('Then', $pattern, $callable, $description);
}
}

View file

@ -0,0 +1,31 @@
<?php
/*
* This file is part of the Behat.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Behat\Behat\Definition\Call;
/**
* When steps definition.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
final class When extends RuntimeDefinition
{
/**
* Initializes definition.
*
* @param string $pattern
* @param callable $callable
* @param null|string $description
*/
public function __construct($pattern, $callable, $description = null)
{
parent::__construct('When', $pattern, $callable, $description);
}
}

View file

@ -0,0 +1,118 @@
<?php
/*
* This file is part of the Behat.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Behat\Behat\Definition\Cli;
use Behat\Behat\Definition\DefinitionWriter;
use Behat\Behat\Definition\Printer\ConsoleDefinitionInformationPrinter;
use Behat\Behat\Definition\Printer\ConsoleDefinitionListPrinter;
use Behat\Behat\Definition\Printer\DefinitionPrinter;
use Behat\Testwork\Cli\Controller;
use Behat\Testwork\Suite\SuiteRepository;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
/**
* Shows all currently available definitions to the user.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
final class AvailableDefinitionsController implements Controller
{
/**
* @var SuiteRepository
*/
private $suiteRepository;
/**
* @var DefinitionWriter
*/
private $writer;
/**
* @var ConsoleDefinitionListPrinter
*/
private $listPrinter;
/**
* @var ConsoleDefinitionInformationPrinter
*/
private $infoPrinter;
/**
* Initializes controller.
*
* @param SuiteRepository $suiteRepository
* @param DefinitionWriter $writer
* @param ConsoleDefinitionListPrinter $listPrinter
* @param ConsoleDefinitionInformationPrinter $infoPrinter
*/
public function __construct(
SuiteRepository $suiteRepository,
DefinitionWriter $writer,
ConsoleDefinitionListPrinter $listPrinter,
ConsoleDefinitionInformationPrinter $infoPrinter
) {
$this->suiteRepository = $suiteRepository;
$this->writer = $writer;
$this->listPrinter = $listPrinter;
$this->infoPrinter = $infoPrinter;
}
/**
* {@inheritdoc}
*/
public function configure(Command $command)
{
$command->addOption('--definitions', '-d', InputOption::VALUE_REQUIRED,
"Print all available step definitions:" . PHP_EOL .
"- use <info>--definitions l</info> to just list definition expressions." . PHP_EOL .
"- use <info>--definitions i</info> to show definitions with extended info." . PHP_EOL .
"- use <info>--definitions 'needle'</info> to find specific definitions." . PHP_EOL .
"Use <info>--lang</info> to see definitions in specific language."
);
}
/**
* {@inheritdoc}
*/
public function execute(InputInterface $input, OutputInterface $output)
{
if (null === $argument = $input->getOption('definitions')) {
return null;
}
$printer = $this->getDefinitionPrinter($argument);
foreach ($this->suiteRepository->getSuites() as $suite) {
$this->writer->printSuiteDefinitions($printer, $suite);
}
return 0;
}
/**
* Returns definition printer for provided option argument.
*
* @param string $argument
*
* @return DefinitionPrinter
*/
private function getDefinitionPrinter($argument)
{
if ('l' === $argument) {
return $this->listPrinter;
}
if ('i' !== $argument) {
$this->infoPrinter->setSearchCriterion($argument);
}
return $this->infoPrinter;
}
}

View file

@ -0,0 +1,52 @@
<?php
/*
* This file is part of the Behat.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Behat\Behat\Definition\Context\Annotation;
use Behat\Behat\Context\Annotation\AnnotationReader;
use ReflectionMethod;
/**
* Reads definition annotations from the context class.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
final class DefinitionAnnotationReader implements AnnotationReader
{
/**
* @var string
*/
private static $regex = '/^\@(given|when|then)\s+(.+)$/i';
/**
* @var string[]
*/
private static $classes = array(
'given' => 'Behat\Behat\Definition\Call\Given',
'when' => 'Behat\Behat\Definition\Call\When',
'then' => 'Behat\Behat\Definition\Call\Then',
);
/**
* {@inheritdoc}
*/
public function readCallee($contextClass, ReflectionMethod $method, $docLine, $description)
{
if (!preg_match(self::$regex, $docLine, $match)) {
return null;
}
$type = strtolower($match[1]);
$class = self::$classes[$type];
$pattern = $match[2];
$callable = array($contextClass, $method->getName());
return new $class($pattern, $callable, $description);
}
}

View file

@ -0,0 +1,42 @@
<?php
/*
* This file is part of the Behat.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Behat\Behat\Definition;
use Behat\Testwork\Call\Callee;
/**
* Represents a step definition.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
interface Definition extends Callee
{
/**
* Returns definition type (Given|When|Then).
*
* @return string
*/
public function getType();
/**
* Returns step pattern exactly as it was defined.
*
* @return string
*/
public function getPattern();
/**
* Represents definition as a string.
*
* @return string
*/
public function __toString();
}

View file

@ -0,0 +1,61 @@
<?php
/*
* This file is part of the Behat.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Behat\Behat\Definition;
use Behat\Behat\Definition\Search\SearchEngine;
use Behat\Gherkin\Node\FeatureNode;
use Behat\Gherkin\Node\StepNode;
use Behat\Testwork\Environment\Environment;
/**
* Finds specific step definition in environment using registered search engines.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
final class DefinitionFinder
{
/**
* @var SearchEngine[]
*/
private $engines = array();
/**
* Registers definition search engine.
*
* @param SearchEngine $searchEngine
*/
public function registerSearchEngine(SearchEngine $searchEngine)
{
$this->engines[] = $searchEngine;
}
/**
* Searches definition for a provided step in a provided environment.
*
* @param Environment $environment
* @param FeatureNode $feature
* @param StepNode $step
*
* @return SearchResult
*/
public function findDefinition(Environment $environment, FeatureNode $feature, StepNode $step)
{
foreach ($this->engines as $engine) {
$result = $engine->searchDefinition($environment, $feature, $step);
if (null !== $result && $result->hasMatch()) {
return $result;
}
}
return new SearchResult();
}
}

View file

@ -0,0 +1,70 @@
<?php
/*
* This file is part of the Behat.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Behat\Behat\Definition;
use Behat\Behat\Definition\Exception\RedundantStepException;
use Behat\Testwork\Environment\Environment;
use Behat\Testwork\Environment\EnvironmentManager;
/**
* Provides step definitions using environment manager.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
final class DefinitionRepository
{
/**
* @var EnvironmentManager
*/
private $environmentManager;
/**
* Initializes repository.
*
* @param EnvironmentManager $environmentManager
*/
public function __construct(EnvironmentManager $environmentManager)
{
$this->environmentManager = $environmentManager;
}
/**
* Returns all available definitions for a specific environment.
*
* @param Environment $environment
*
* @return Definition[]
*
* @throws RedundantStepException
*/
public function getEnvironmentDefinitions(Environment $environment)
{
$patterns = array();
$definitions = array();
foreach ($this->environmentManager->readEnvironmentCallees($environment) as $callee) {
if (!$callee instanceof Definition) {
continue;
}
$pattern = $callee->getPattern();
if (isset($patterns[$pattern])) {
throw new RedundantStepException($callee, $patterns[$pattern]);
}
$patterns[$pattern] = $callee;
$definitions[] = $callee;
}
return $definitions;
}
}

View file

@ -0,0 +1,58 @@
<?php
/*
* This file is part of the Behat.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Behat\Behat\Definition;
use Behat\Behat\Definition\Printer\DefinitionPrinter;
use Behat\Testwork\Environment\EnvironmentManager;
use Behat\Testwork\Suite\Suite;
/**
* Prints definitions using provided printer.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
final class DefinitionWriter
{
/**
* @var EnvironmentManager
*/
private $environmentManager;
/**
* @var DefinitionRepository
*/
private $repository;
/**
* Initializes writer.
*
* @param EnvironmentManager $environmentManager
* @param DefinitionRepository $repository
*/
public function __construct(EnvironmentManager $environmentManager, DefinitionRepository $repository)
{
$this->environmentManager = $environmentManager;
$this->repository = $repository;
}
/**
* Prints definitions for provided suite using printer.
*
* @param DefinitionPrinter $printer
* @param Suite $suite
*/
public function printSuiteDefinitions(DefinitionPrinter $printer, $suite)
{
$environment = $this->environmentManager->buildEnvironment($suite);
$definitions = $this->repository->getEnvironmentDefinitions($environment);
$printer->printDefinitions($suite, $definitions);
}
}

View file

@ -0,0 +1,57 @@
<?php
/*
* This file is part of the Behat.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Behat\Behat\Definition\Exception;
use Behat\Behat\Definition\Definition;
use RuntimeException;
/**
* Represents an exception caused by an ambiguous step definition match.
*
* If multiple definitions match the same step, behat is not able to determine which one is better and thus this
* exception is thrown and test suite is stopped.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
final class AmbiguousMatchException extends RuntimeException implements SearchException
{
/**
* @var string
*/
private $text;
/**
* @var Definition[]
*/
private $matches = array();
/**
* Initializes ambiguous exception.
*
* @param string $text step description
* @param Definition[] $matches ambiguous matches (array of Definition's)
*/
public function __construct($text, array $matches)
{
$this->text = $text;
$this->matches = $matches;
$message = sprintf("Ambiguous match of \"%s\":", $text);
foreach ($matches as $definition) {
$message .= sprintf(
"\nto `%s` from %s",
$definition->getPattern(),
$definition->getPath()
);
}
parent::__construct($message);
}
}

View file

@ -0,0 +1,22 @@
<?php
/*
* This file is part of the Behat.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Behat\Behat\Definition\Exception;
use Behat\Testwork\Exception\TestworkException;
/**
* Represents an exception thrown during step definition handling.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
interface DefinitionException extends TestworkException
{
}

View file

@ -0,0 +1,22 @@
<?php
/*
* This file is part of the Behat.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Behat\Behat\Definition\Exception;
use InvalidArgumentException;
/**
* Represents an exception caused by an invalid definition pattern (not able to transform it to a regex).
*
* @author Christophe Coevoet <stof@notk.org>
*/
final class InvalidPatternException extends InvalidArgumentException implements DefinitionException
{
}

View file

@ -0,0 +1,41 @@
<?php
/*
* This file is part of the Behat.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Behat\Behat\Definition\Exception;
use Behat\Behat\Definition\Definition;
use RuntimeException;
/**
* Represents an exception caused by a redundant step definition.
*
* If multiple step definitions in the boundaries of the same suite use same regular expression, behat is not able
* to determine which one is better and thus this exception is thrown and test suite is stopped.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
final class RedundantStepException extends RuntimeException implements SearchException
{
/**
* Initializes redundant exception.
*
* @param Definition $step2 duplicate step definition
* @param Definition $step1 firstly matched step definition
*/
public function __construct(Definition $step2, Definition $step1)
{
$message = sprintf(
"Step \"%s\" is already defined in %s\n\n%s\n%s",
$step2->getPattern(), $step1->getPath(), $step1->getPath(), $step2->getPath()
);
parent::__construct($message);
}
}

View file

@ -0,0 +1,20 @@
<?php
/*
* This file is part of the Behat.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Behat\Behat\Definition\Exception;
/**
* Represents an exception caused by a definition search.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
interface SearchException extends DefinitionException
{
}

View file

@ -0,0 +1,49 @@
<?php
/*
* This file is part of the Behat.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Behat\Behat\Definition\Exception;
use InvalidArgumentException;
/**
* Represents an exception caused by an unrecognised definition pattern.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
final class UnknownPatternException extends InvalidArgumentException implements DefinitionException
{
/**
* @var string
*/
private $pattern;
/**
* Initializes exception.
*
* @param string $message
* @param integer $pattern
*/
public function __construct($message, $pattern)
{
$this->pattern = $pattern;
parent::__construct($message);
}
/**
* Returns pattern that caused exception.
*
* @return string
*/
public function getPattern()
{
return $this->pattern;
}
}

View file

@ -0,0 +1,49 @@
<?php
/*
* This file is part of the Behat.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Behat\Behat\Definition\Exception;
use InvalidArgumentException;
/**
* Represents an exception caused by an unsupported pattern type.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
final class UnsupportedPatternTypeException extends InvalidArgumentException implements DefinitionException
{
/**
* @var string
*/
private $type;
/**
* Initializes exception.
*
* @param string $message
* @param string $type
*/
public function __construct($message, $type)
{
$this->type = $type;
parent::__construct($message);
}
/**
* Returns pattern type that caused exception.
*
* @return string
*/
public function getType()
{
return $this->type;
}
}

View file

@ -0,0 +1,76 @@
<?php
/*
* This file is part of the Behat.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Behat\Behat\Definition\Pattern;
/**
* Step definition pattern.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
final class Pattern
{
/**
* @var string
*/
private $canonicalText;
/**
* @var string
*/
private $pattern;
/**
* @var integer
*/
private $placeholderCount;
/**
* Initializes pattern.
*
* @param string $canonicalText
* @param string $pattern
* @param integer $placeholderCount
*/
public function __construct($canonicalText, $pattern, $placeholderCount = 0)
{
$this->canonicalText = $canonicalText;
$this->pattern = $pattern;
$this->placeholderCount = $placeholderCount;
}
/**
* Returns canonical step text.
*
* @return string
*/
public function getCanonicalText()
{
return $this->canonicalText;
}
/**
* Returns pattern.
*
* @return string
*/
public function getPattern()
{
return $this->pattern;
}
/**
* Returns pattern placeholder count.
*
* @return integer
*/
public function getPlaceholderCount()
{
return $this->placeholderCount;
}
}

View file

@ -0,0 +1,79 @@
<?php
/*
* This file is part of the Behat.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Behat\Behat\Definition\Pattern;
use Behat\Behat\Definition\Exception\UnknownPatternException;
use Behat\Behat\Definition\Exception\UnsupportedPatternTypeException;
use Behat\Behat\Definition\Pattern\Policy\PatternPolicy;
/**
* Transforms patterns using registered policies.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
final class PatternTransformer
{
/**
* @var PatternPolicy[]
*/
private $policies = array();
/**
* Registers pattern policy.
*
* @param PatternPolicy $policy
*/
public function registerPatternPolicy(PatternPolicy $policy)
{
$this->policies[] = $policy;
}
/**
* Generates pattern.
*
* @param string $type
* @param string $stepText
*
* @return Pattern
*
* @throws UnsupportedPatternTypeException
*/
public function generatePattern($type, $stepText)
{
foreach ($this->policies as $policy) {
if ($policy->supportsPatternType($type)) {
return $policy->generatePattern($stepText);
}
}
throw new UnsupportedPatternTypeException(sprintf('Can not find policy for a pattern type `%s`.', $type), $type);
}
/**
* Transforms pattern string to regex.
*
* @param string $pattern
*
* @return string
*
* @throws UnknownPatternException
*/
public function transformPatternToRegex($pattern)
{
foreach ($this->policies as $policy) {
if ($policy->supportsPattern($pattern)) {
return $policy->transformPatternToRegex($pattern);
}
}
throw new UnknownPatternException(sprintf('Can not find policy for a pattern `%s`.', $pattern), $pattern);
}
}

View file

@ -0,0 +1,60 @@
<?php
/*
* This file is part of the Behat.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Behat\Behat\Definition\Pattern\Policy;
use Behat\Behat\Definition\Pattern\Pattern;
use Behat\Behat\Definition\Pattern\PatternTransformer;
/**
* Defines a way to handle custom definition patterns.
*
* @see PatternTransformer
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
interface PatternPolicy
{
/**
* Checks if policy supports pattern type.
*
* @param string $type
*
* @return Boolean
*/
public function supportsPatternType($type);
/**
* Generates pattern for step text.
*
* @param string $stepText
*
* @return Pattern
*/
public function generatePattern($stepText);
/**
* Checks if policy supports pattern.
*
* @param string $pattern
*
* @return Boolean
*/
public function supportsPattern($pattern);
/**
* Transforms pattern string to regex.
*
* @param string $pattern
*
* @return string
*/
public function transformPatternToRegex($pattern);
}

View file

@ -0,0 +1,135 @@
<?php
/*
* This file is part of the Behat.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Behat\Behat\Definition\Pattern\Policy;
use Behat\Behat\Definition\Exception\InvalidPatternException;
use Behat\Behat\Definition\Pattern\Pattern;
use Behat\Transliterator\Transliterator;
/**
* Defines a way to handle regex patterns.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
final class RegexPatternPolicy implements PatternPolicy
{
/**
* @var string[string]
*/
private static $replacePatterns = array(
"/(?<=\W|^)\\\'(?:((?!\\').)*)\\\'(?=\W|$)/" => "'([^']*)'", // Single quoted strings
'/(?<=\W|^)\"(?:[^\"]*)\"(?=\W|$)/' => "\"([^\"]*)\"", // Double quoted strings
'/(?<=\W|^)(\d+)(?=\W|$)/' => "(\\d+)", // Numbers
);
/**
* {@inheritdoc}
*/
public function supportsPatternType($type)
{
return 'regex' === $type;
}
/**
* {@inheritdoc}
*/
public function generatePattern($stepText)
{
$canonicalText = $this->generateCanonicalText($stepText);
$stepRegex = $this->generateRegex($stepText);
$placeholderCount = $this->countPlaceholders($stepText, $stepRegex);
return new Pattern($canonicalText, '/^' . $stepRegex . '$/', $placeholderCount);
}
/**
* {@inheritdoc}
*/
public function supportsPattern($pattern)
{
return (bool) preg_match('/^(?:\\{.*\\}|([~\\/#`]).*\1)[imsxADSUXJu]*$/s', $pattern);
}
/**
* {@inheritdoc}
*/
public function transformPatternToRegex($pattern)
{
if (false === @preg_match($pattern, 'anything')) {
$error = error_get_last();
$errorMessage = isset($error['message']) ? $error['message'] : '';
throw new InvalidPatternException(sprintf('The regex `%s` is invalid: %s', $pattern, $errorMessage));
}
return $pattern;
}
/**
* Generates regex from step text.
*
* @param string $stepText
*
* @return string
*/
private function generateRegex($stepText)
{
return preg_replace(
array_keys(self::$replacePatterns),
array_values(self::$replacePatterns),
$this->escapeStepText($stepText)
);
}
/**
* Generates canonical text for step text.
*
* @param string $stepText
*
* @return string
*/
private function generateCanonicalText($stepText)
{
$canonicalText = preg_replace(array_keys(self::$replacePatterns), '', $stepText);
$canonicalText = Transliterator::transliterate($canonicalText, ' ');
$canonicalText = preg_replace('/[^a-zA-Z\_\ ]/', '', $canonicalText);
$canonicalText = str_replace(' ', '', ucwords($canonicalText));
return $canonicalText;
}
/**
* Counts regex placeholders using provided text.
*
* @param string $stepText
* @param string $stepRegex
*
* @return integer
*/
private function countPlaceholders($stepText, $stepRegex)
{
preg_match('/^' . $stepRegex . '$/', $stepText, $matches);
return count($matches) ? count($matches) - 1 : 0;
}
/**
* Returns escaped step text.
*
* @param string $stepText
*
* @return string
*/
private function escapeStepText($stepText)
{
return preg_replace('/([\/\[\]\(\)\\\^\$\.\|\?\*\+\'])/', '\\\\$1', $stepText);
}
}

View file

@ -0,0 +1,212 @@
<?php
/*
* This file is part of the Behat.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Behat\Behat\Definition\Pattern\Policy;
use Behat\Behat\Definition\Pattern\Pattern;
use Behat\Behat\Definition\Exception\InvalidPatternException;
use Behat\Transliterator\Transliterator;
/**
* Defines a way to handle turnip patterns.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
final class TurnipPatternPolicy implements PatternPolicy
{
const TOKEN_REGEX = "[\"']?(?P<%s>(?<=\")[^\"]*(?=\")|(?<=')[^']*(?=')|\-?[\w\.\,]+)['\"]?";
const PLACEHOLDER_REGEXP = "/\\\:(\w+)/";
const OPTIONAL_WORD_REGEXP = '/(\s)?\\\\\(([^\\\]+)\\\\\)(\s)?/';
const ALTERNATIVE_WORD_REGEXP = '/(\w+)\\\\\/(\w+)/';
/**
* @var string[]
*/
private $regexCache = array();
/**
* @var string[]
*/
private static $placeholderPatterns = array(
"/(?<!\w)\"[^\"]+\"(?!\w)/",
"/(?<!\w)'[^']+'(?!\w)/",
"/(?<!\w|\.|\,)\-?\d+(?:[\.\,]\d+)?(?!\w|\.|\,)/"
);
/**
* {@inheritdoc}
*/
public function supportsPatternType($type)
{
return null === $type || 'turnip' === $type;
}
/**
* {@inheritdoc}
*/
public function generatePattern($stepText)
{
$count = 0;
$pattern = $stepText;
foreach (self::$placeholderPatterns as $replacePattern) {
$pattern = preg_replace_callback(
$replacePattern,
function () use (&$count) { return ':arg' . ++$count; },
$pattern
);
}
$pattern = $this->escapeAlternationSyntax($pattern);
$canonicalText = $this->generateCanonicalText($stepText);
return new Pattern($canonicalText, $pattern, $count);
}
/**
* {@inheritdoc}
*/
public function supportsPattern($pattern)
{
return true;
}
/**
* {@inheritdoc}
*/
public function transformPatternToRegex($pattern)
{
if (!isset($this->regexCache[$pattern])) {
$this->regexCache[$pattern] = $this->createTransformedRegex($pattern);
}
return $this->regexCache[$pattern];
}
/**
* @param string $pattern
* @return string
*/
private function createTransformedRegex($pattern)
{
$regex = preg_quote($pattern, '/');
$regex = $this->replaceTokensWithRegexCaptureGroups($regex);
$regex = $this->replaceTurnipOptionalEndingWithRegex($regex);
$regex = $this->replaceTurnipAlternativeWordsWithRegex($regex);
return '/^' . $regex . '$/i';
}
/**
* Generates canonical text for step text.
*
* @param string $stepText
*
* @return string
*/
private function generateCanonicalText($stepText)
{
$canonicalText = preg_replace(self::$placeholderPatterns, '', $stepText);
$canonicalText = Transliterator::transliterate($canonicalText, ' ');
$canonicalText = preg_replace('/[^a-zA-Z\_\ ]/', '', $canonicalText);
$canonicalText = str_replace(' ', '', ucwords($canonicalText));
return $canonicalText;
}
/**
* Replaces turnip tokens with regex capture groups.
*
* @param string $regex
*
* @return string
*/
private function replaceTokensWithRegexCaptureGroups($regex)
{
$tokenRegex = self::TOKEN_REGEX;
return preg_replace_callback(
self::PLACEHOLDER_REGEXP,
array($this, 'replaceTokenWithRegexCaptureGroup'),
$regex
);
}
private function replaceTokenWithRegexCaptureGroup($tokenMatch)
{
if (strlen($tokenMatch[1]) >= 32) {
throw new InvalidPatternException(
"Token name should not exceed 32 characters, but `{$tokenMatch[1]}` was used."
);
}
return sprintf(self::TOKEN_REGEX, $tokenMatch[1]);
}
/**
* Replaces turnip optional ending with regex non-capturing optional group.
*
* @param string $regex
*
* @return string
*/
private function replaceTurnipOptionalEndingWithRegex($regex)
{
return preg_replace(self::OPTIONAL_WORD_REGEXP, '(?:\1)?(?:\2)?(?:\3)?', $regex);
}
/**
* Replaces turnip alternative words with regex non-capturing alternating group.
*
* @param string $regex
*
* @return string
*/
private function replaceTurnipAlternativeWordsWithRegex($regex)
{
$regex = preg_replace(self::ALTERNATIVE_WORD_REGEXP, '(?:\1|\2)', $regex);
$regex = $this->removeEscapingOfAlternationSyntax($regex);
return $regex;
}
/**
* Adds escaping to alternation syntax in pattern.
*
* By default, Turnip treats `/` as alternation syntax. Meaning `one/two` for Turnip
* means either `one` or `two`. Sometimes though you'll want to use slash character
* with different purpose (URL, UNIX paths). In this case, you would escape slashes
* with backslash.
*
* This method adds escaping to all slashes in generated snippets.
*
* @param string $pattern
*
* @return string
*/
private function escapeAlternationSyntax($pattern)
{
return str_replace('/', '\/', $pattern);
}
/**
* Removes escaping of alternation syntax from regex.
*
* This method removes those escaping backslashes from your slashes, so your steps
* could be matched against your escaped definitions.
*
* @param string $regex
*
* @return string
*/
private function removeEscapingOfAlternationSyntax($regex)
{
return str_replace('\\\/', '/', $regex);
}
}

View file

@ -0,0 +1,136 @@
<?php
/*
* This file is part of the Behat.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Behat\Behat\Definition\Printer;
use Behat\Behat\Definition\Definition;
use Behat\Testwork\Suite\Suite;
/**
* Prints definitions with full information about them.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
final class ConsoleDefinitionInformationPrinter extends ConsoleDefinitionPrinter
{
/**
* @var null|string
*/
private $searchCriterion;
/**
* Sets search criterion.
*
* @param string $criterion
*/
public function setSearchCriterion($criterion)
{
$this->searchCriterion = $criterion;
}
/**
* {@inheritdoc}
*/
public function printDefinitions(Suite $suite, $definitions)
{
$search = $this->searchCriterion;
$output = array();
foreach ($definitions as $definition) {
$definition = $this->translateDefinition($suite, $definition);
$pattern = $definition->getPattern();
if (null !== $search && false === mb_strpos($pattern, $search, 0, 'utf8')) {
continue;
}
$lines = array_merge(
$this->extractHeader($suite, $definition),
$this->extractDescription($suite, $definition),
$this->extractFooter($suite, $definition)
);
$output[] = implode(PHP_EOL, $lines) . PHP_EOL;
}
$this->write(rtrim(implode(PHP_EOL, $output)));
}
/**
* Extracts the formatted header from the definition.
*
* @param Suite $suite
* @param Definition $definition
*
* @return string[]
*/
private function extractHeader(Suite $suite, Definition $definition)
{
$pattern = $definition->getPattern();
$lines = array();
$lines[] = strtr(
'{suite} <def_dimmed>|</def_dimmed> <info>{type}</info> <def_regex>{regex}</def_regex>', array(
'{suite}' => $suite->getName(),
'{type}' => $this->getDefinitionType($definition),
'{regex}' => $pattern,
)
);
return $lines;
}
/**
* Extracts the formatted description from the definition.
*
* @param Suite $suite
* @param Definition $definition
*
* @return string[]
*/
private function extractDescription(Suite $suite, Definition $definition)
{
$definition = $this->translateDefinition($suite, $definition);
$lines = array();
if ($description = $definition->getDescription()) {
foreach (explode("\n", $description) as $descriptionLine) {
$lines[] = strtr(
'{space}<def_dimmed>|</def_dimmed> {description}', array(
'{space}' => str_pad('', mb_strlen($suite->getName(), 'utf8') + 1),
'{description}' => $descriptionLine
)
);
}
}
return $lines;
}
/**
* Extracts the formatted footer from the definition.
*
* @param Suite $suite
* @param Definition $definition
*
* @return string[]
*/
private function extractFooter(Suite $suite, Definition $definition)
{
$lines = array();
$lines[] = strtr(
'{space}<def_dimmed>|</def_dimmed> at `{path}`', array(
'{space}' => str_pad('', mb_strlen($suite->getName(), 'utf8') + 1),
'{path}' => $definition->getPath()
)
);
return $lines;
}
}

View file

@ -0,0 +1,43 @@
<?php
/*
* This file is part of the Behat.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Behat\Behat\Definition\Printer;
use Behat\Testwork\Suite\Suite;
/**
* Prints simple definitions list.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
final class ConsoleDefinitionListPrinter extends ConsoleDefinitionPrinter
{
/**
* {@inheritdoc}
*/
public function printDefinitions(Suite $suite, $definitions)
{
$output = array();
foreach ($definitions as $definition) {
$definition = $this->translateDefinition($suite, $definition);
$output[] = strtr(
'{suite} <def_dimmed>|</def_dimmed> <info>{type}</info> <def_regex>{regex}</def_regex>', array(
'{suite}' => $suite->getName(),
'{type}' => $this->getDefinitionType($definition, true),
'{regex}' => $definition->getPattern(),
)
);
}
$this->write(rtrim(implode(PHP_EOL, $output)));
}
}

View file

@ -0,0 +1,113 @@
<?php
/*
* This file is part of the Behat.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Behat\Behat\Definition\Printer;
use Behat\Behat\Definition\Definition;
use Behat\Behat\Definition\Pattern\PatternTransformer;
use Behat\Behat\Definition\Translator\DefinitionTranslator;
use Behat\Gherkin\Keywords\KeywordsInterface;
use Behat\Testwork\Suite\Suite;
use Symfony\Component\Console\Formatter\OutputFormatterStyle;
use Symfony\Component\Console\Output\OutputInterface;
/**
* Represents console-based definition printer.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
abstract class ConsoleDefinitionPrinter implements DefinitionPrinter
{
/**
* @var OutputInterface
*/
private $output;
/**
* @var PatternTransformer
*/
private $patternTransformer;
/**
* @var DefinitionTranslator
*/
private $translator;
/**
* @var KeywordsInterface
*/
private $keywords;
/**
* Initializes printer.
*
* @param OutputInterface $output
* @param PatternTransformer $patternTransformer
* @param DefinitionTranslator $translator
* @param KeywordsInterface $keywords
*/
public function __construct(
OutputInterface $output,
PatternTransformer $patternTransformer,
DefinitionTranslator $translator,
KeywordsInterface $keywords
) {
$this->output = $output;
$this->patternTransformer = $patternTransformer;
$this->translator = $translator;
$this->keywords = $keywords;
$output->getFormatter()->setStyle('def_regex', new OutputFormatterStyle('yellow'));
$output->getFormatter()->setStyle(
'def_regex_capture',
new OutputFormatterStyle('yellow', null, array('bold'))
);
$output->getFormatter()->setStyle(
'def_dimmed',
new OutputFormatterStyle('black', null, array('bold'))
);
}
/**
* Writes text to the console.
*
* @param string $text
*/
final protected function write($text)
{
$this->output->writeln($text);
$this->output->writeln('');
}
final protected function getDefinitionType(Definition $definition, $onlyOne = false)
{
$this->keywords->setLanguage($this->translator->getLocale());
$method = 'get'.ucfirst($definition->getType()).'Keywords';
$keywords = explode('|', $this->keywords->$method());
if ($onlyOne) {
return current($keywords);
}
return 1 < count($keywords) ? '['.implode('|', $keywords).']' : implode('|', $keywords);
}
/**
* Translates definition using translator.
*
* @param Suite $suite
* @param Definition $definition
*
* @return Definition
*/
final protected function translateDefinition(Suite $suite, Definition $definition)
{
return $this->translator->translateDefinition($suite, $definition);
}
}

View file

@ -0,0 +1,30 @@
<?php
/*
* This file is part of the Behat.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Behat\Behat\Definition\Printer;
use Behat\Behat\Definition\Definition;
use Behat\Testwork\Suite\Suite;
/**
* Prints provided definition.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
interface DefinitionPrinter
{
/**
* Prints definition.
*
* @param Suite $suite
* @param Definition[] $definitions
*/
public function printDefinitions(Suite $suite, $definitions);
}

View file

@ -0,0 +1,130 @@
<?php
/*
* This file is part of the Behat.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Behat\Behat\Definition\Search;
use Behat\Behat\Definition\Definition;
use Behat\Behat\Definition\DefinitionRepository;
use Behat\Behat\Definition\Exception\AmbiguousMatchException;
use Behat\Behat\Definition\Pattern\PatternTransformer;
use Behat\Behat\Definition\SearchResult;
use Behat\Behat\Definition\Translator\DefinitionTranslator;
use Behat\Gherkin\Node\ArgumentInterface;
use Behat\Gherkin\Node\FeatureNode;
use Behat\Gherkin\Node\StepNode;
use Behat\Testwork\Argument\ArgumentOrganiser;
use Behat\Testwork\Environment\Environment;
/**
* Searches for a step definition using definition repository.
*
* @see DefinitionRepository
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
final class RepositorySearchEngine implements SearchEngine
{
/**
* @var DefinitionRepository
*/
private $repository;
/**
* @var PatternTransformer
*/
private $patternTransformer;
/**
* @var DefinitionTranslator
*/
private $translator;
/**
* @var ArgumentOrganiser
*/
private $argumentOrganiser;
/**
* Initializes search engine.
*
* @param DefinitionRepository $repository
* @param PatternTransformer $patternTransformer
* @param DefinitionTranslator $translator
* @param ArgumentOrganiser $argumentOrganiser
*/
public function __construct(
DefinitionRepository $repository,
PatternTransformer $patternTransformer,
DefinitionTranslator $translator,
ArgumentOrganiser $argumentOrganiser
) {
$this->repository = $repository;
$this->patternTransformer = $patternTransformer;
$this->translator = $translator;
$this->argumentOrganiser = $argumentOrganiser;
}
/**
* {@inheritdoc}
*
* @throws AmbiguousMatchException
*/
public function searchDefinition(
Environment $environment,
FeatureNode $feature,
StepNode $step
) {
$suite = $environment->getSuite();
$language = $feature->getLanguage();
$stepText = $step->getText();
$multi = $step->getArguments();
$definitions = array();
$result = null;
foreach ($this->repository->getEnvironmentDefinitions($environment) as $definition) {
$definition = $this->translator->translateDefinition($suite, $definition, $language);
if (!$newResult = $this->match($definition, $stepText, $multi)) {
continue;
}
$result = $newResult;
$definitions[] = $newResult->getMatchedDefinition();
}
if (count($definitions) > 1) {
throw new AmbiguousMatchException($result->getMatchedText(), $definitions);
}
return $result;
}
/**
* Attempts to match provided definition against a step text.
*
* @param Definition $definition
* @param string $stepText
* @param ArgumentInterface[] $multiline
*
* @return null|SearchResult
*/
private function match(Definition $definition, $stepText, array $multiline)
{
$regex = $this->patternTransformer->transformPatternToRegex($definition->getPattern());
if (!preg_match($regex, $stepText, $match)) {
return null;
}
$function = $definition->getReflection();
$match = array_merge($match, array_values($multiline));
$arguments = $this->argumentOrganiser->organiseArguments($function, $match);
return new SearchResult($definition, $stepText, $arguments);
}
}

View file

@ -0,0 +1,38 @@
<?php
/*
* This file is part of the Behat.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Behat\Behat\Definition\Search;
use Behat\Behat\Definition\DefinitionFinder;
use Behat\Behat\Definition\SearchResult;
use Behat\Gherkin\Node\FeatureNode;
use Behat\Gherkin\Node\StepNode;
use Behat\Testwork\Environment\Environment;
/**
* Searches for a step definition in a specific environment.
*
* @see DefinitionFinder
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
interface SearchEngine
{
/**
* Searches for a step definition.
*
* @param Environment $environment
* @param FeatureNode $feature
* @param StepNode $step
*
* @return null|SearchResult
*/
public function searchDefinition(Environment $environment, FeatureNode $feature, StepNode $step);
}

View file

@ -0,0 +1,86 @@
<?php
/*
* This file is part of the Behat.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Behat\Behat\Definition;
/**
* Step definition search result.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
final class SearchResult
{
/**
* @var null|Definition
*/
private $definition;
/**
* @var null|string
*/
private $matchedText;
/**
* @var null|array
*/
private $arguments;
/**
* Registers search match.
*
* @param null|Definition $definition
* @param null|string $matchedText
* @param null|array $arguments
*/
public function __construct(Definition $definition = null, $matchedText = null, array $arguments = null)
{
$this->definition = $definition;
$this->matchedText = $matchedText;
$this->arguments = $arguments;
}
/**
* Checks if result contains a match.
*
* @return Boolean
*/
public function hasMatch()
{
return null !== $this->definition;
}
/**
* Returns matched definition.
*
* @return null|Definition
*/
public function getMatchedDefinition()
{
return $this->definition;
}
/**
* Returns matched text.
*
* @return null|string
*/
public function getMatchedText()
{
return $this->matchedText;
}
/**
* Returns matched definition arguments.
*
* @return null|array
*/
public function getMatchedArguments()
{
return $this->arguments;
}
}

View file

@ -0,0 +1,310 @@
<?php
/*
* This file is part of the Behat.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Behat\Behat\Definition\ServiceContainer;
use Behat\Behat\Context\ServiceContainer\ContextExtension;
use Behat\Testwork\Argument\ServiceContainer\ArgumentExtension;
use Behat\Behat\Gherkin\ServiceContainer\GherkinExtension;
use Behat\Testwork\Cli\ServiceContainer\CliExtension;
use Behat\Testwork\Environment\ServiceContainer\EnvironmentExtension;
use Behat\Testwork\ServiceContainer\Extension;
use Behat\Testwork\ServiceContainer\ExtensionManager;
use Behat\Testwork\ServiceContainer\ServiceProcessor;
use Behat\Testwork\Suite\ServiceContainer\SuiteExtension;
use Behat\Testwork\Translator\ServiceContainer\TranslatorExtension;
use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference;
/**
* Extends Behat with definition services.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
final class DefinitionExtension implements Extension
{
/*
* Available services
*/
const FINDER_ID = 'definition.finder';
const REPOSITORY_ID = 'definition.repository';
const PATTERN_TRANSFORMER_ID = 'definition.pattern_transformer';
const WRITER_ID = 'definition.writer';
const DEFINITION_TRANSLATOR_ID = 'definition.translator';
/*
* Available extension points
*/
const SEARCH_ENGINE_TAG = 'definition.search_engine';
const PATTERN_POLICY_TAG = 'definition.pattern_policy';
/**
* @var ServiceProcessor
*/
private $processor;
/**
* Initializes compiler pass.
*
* @param null|ServiceProcessor $processor
*/
public function __construct(ServiceProcessor $processor = null)
{
$this->processor = $processor ? : new ServiceProcessor();
}
/**
* {@inheritdoc}
*/
public function getConfigKey()
{
return 'definitions';
}
/**
* {@inheritdoc}
*/
public function initialize(ExtensionManager $extensionManager)
{
}
/**
* {@inheritdoc}
*/
public function configure(ArrayNodeDefinition $builder)
{
}
/**
* {@inheritdoc}
*/
public function load(ContainerBuilder $container, array $config)
{
$this->loadFinder($container);
$this->loadRepository($container);
$this->loadWriter($container);
$this->loadPatternTransformer($container);
$this->loadDefinitionTranslator($container);
$this->loadDefaultSearchEngines($container);
$this->loadDefaultPatternPolicies($container);
$this->loadAnnotationReader($container);
$this->loadDefinitionPrinters($container);
$this->loadController($container);
}
/**
* {@inheritdoc}
*/
public function process(ContainerBuilder $container)
{
$this->processSearchEngines($container);
$this->processPatternPolicies($container);
}
/**
* Loads definition finder.
*
* @param ContainerBuilder $container
*/
private function loadFinder(ContainerBuilder $container)
{
$definition = new Definition('Behat\Behat\Definition\DefinitionFinder');
$container->setDefinition(self::FINDER_ID, $definition);
}
/**
* Loads definition repository.
*
* @param ContainerBuilder $container
*/
private function loadRepository(ContainerBuilder $container)
{
$definition = new Definition('Behat\Behat\Definition\DefinitionRepository', array(
new Reference(EnvironmentExtension::MANAGER_ID)
));
$container->setDefinition(self::REPOSITORY_ID, $definition);
}
/**
* Loads definition writer.
*
* @param ContainerBuilder $container
*/
private function loadWriter(ContainerBuilder $container)
{
$definition = new Definition('Behat\Behat\Definition\DefinitionWriter', array(
new Reference(EnvironmentExtension::MANAGER_ID),
new Reference(self::REPOSITORY_ID)
));
$container->setDefinition(self::WRITER_ID, $definition);
}
/**
* Loads definition pattern transformer.
*
* @param ContainerBuilder $container
*/
private function loadPatternTransformer(ContainerBuilder $container)
{
$definition = new Definition('Behat\Behat\Definition\Pattern\PatternTransformer');
$container->setDefinition(self::PATTERN_TRANSFORMER_ID, $definition);
}
/**
* Loads definition translator.
*
* @param ContainerBuilder $container
*/
private function loadDefinitionTranslator(ContainerBuilder $container)
{
$definition = new Definition('Behat\Behat\Definition\Translator\DefinitionTranslator', array(
new Reference(TranslatorExtension::TRANSLATOR_ID)
));
$container->setDefinition(self::DEFINITION_TRANSLATOR_ID, $definition);
}
/**
* Loads default search engines.
*
* @param ContainerBuilder $container
*/
private function loadDefaultSearchEngines(ContainerBuilder $container)
{
$definition = new Definition('Behat\Behat\Definition\Search\RepositorySearchEngine', array(
new Reference(self::REPOSITORY_ID),
new Reference(self::PATTERN_TRANSFORMER_ID),
new Reference(self::DEFINITION_TRANSLATOR_ID),
new Reference(ArgumentExtension::PREG_MATCH_ARGUMENT_ORGANISER_ID)
));
$definition->addTag(self::SEARCH_ENGINE_TAG, array('priority' => 50));
$container->setDefinition(self::SEARCH_ENGINE_TAG . '.repository', $definition);
}
/**
* Loads default pattern policies.
*
* @param ContainerBuilder $container
*/
private function loadDefaultPatternPolicies(ContainerBuilder $container)
{
$definition = new Definition('Behat\Behat\Definition\Pattern\Policy\TurnipPatternPolicy');
$definition->addTag(self::PATTERN_POLICY_TAG, array('priority' => 50));
$container->setDefinition(self::PATTERN_POLICY_TAG . '.turnip', $definition);
$definition = new Definition('Behat\Behat\Definition\Pattern\Policy\RegexPatternPolicy');
$definition->addTag(self::PATTERN_POLICY_TAG, array('priority' => 60));
$container->setDefinition(self::PATTERN_POLICY_TAG . '.regex', $definition);
}
/**
* Loads definition annotation reader.
*
* @param ContainerBuilder $container
*/
private function loadAnnotationReader(ContainerBuilder $container)
{
$definition = new Definition('Behat\Behat\Definition\Context\Annotation\DefinitionAnnotationReader');
$definition->addTag(ContextExtension::ANNOTATION_READER_TAG, array('priority' => 50));
$container->setDefinition(ContextExtension::ANNOTATION_READER_TAG . '.definition', $definition);
}
/**
* Loads definition printers.
*
* @param ContainerBuilder $container
*/
private function loadDefinitionPrinters(ContainerBuilder $container)
{
$definition = new Definition('Behat\Behat\Definition\Printer\ConsoleDefinitionInformationPrinter', array(
new Reference(CliExtension::OUTPUT_ID),
new Reference(self::PATTERN_TRANSFORMER_ID),
new Reference(self::DEFINITION_TRANSLATOR_ID),
new Reference(GherkinExtension::KEYWORDS_ID)
));
$container->setDefinition($this->getInformationPrinterId(), $definition);
$definition = new Definition('Behat\Behat\Definition\Printer\ConsoleDefinitionListPrinter', array(
new Reference(CliExtension::OUTPUT_ID),
new Reference(self::PATTERN_TRANSFORMER_ID),
new Reference(self::DEFINITION_TRANSLATOR_ID),
new Reference(GherkinExtension::KEYWORDS_ID)
));
$container->setDefinition($this->getListPrinterId(), $definition);
}
/**
* Loads definition controller.
*
* @param ContainerBuilder $container
*/
private function loadController(ContainerBuilder $container)
{
$definition = new Definition('Behat\Behat\Definition\Cli\AvailableDefinitionsController', array(
new Reference(SuiteExtension::REGISTRY_ID),
new Reference(self::WRITER_ID),
new Reference($this->getListPrinterId()),
new Reference($this->getInformationPrinterId())
));
$definition->addTag(CliExtension::CONTROLLER_TAG, array('priority' => 500));
$container->setDefinition(CliExtension::CONTROLLER_TAG . '.available_definitions', $definition);
}
/**
* Processes all search engines in the container.
*
* @param ContainerBuilder $container
*/
private function processSearchEngines(ContainerBuilder $container)
{
$references = $this->processor->findAndSortTaggedServices($container, self::SEARCH_ENGINE_TAG);
$definition = $container->getDefinition(self::FINDER_ID);
foreach ($references as $reference) {
$definition->addMethodCall('registerSearchEngine', array($reference));
}
}
/**
* Processes all pattern policies.
*
* @param ContainerBuilder $container
*/
private function processPatternPolicies(ContainerBuilder $container)
{
$references = $this->processor->findAndSortTaggedServices($container, self::PATTERN_POLICY_TAG);
$definition = $container->getDefinition(self::PATTERN_TRANSFORMER_ID);
foreach ($references as $reference) {
$definition->addMethodCall('registerPatternPolicy', array($reference));
}
}
/**
* returns list printer service id.
*
* @return string
*/
private function getListPrinterId()
{
return 'definition.list_printer';
}
/**
* Returns information printer service id.
*
* @return string
*/
private function getInformationPrinterId()
{
return 'definition.information_printer';
}
}

View file

@ -0,0 +1,65 @@
<?php
/*
* This file is part of the Behat.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Behat\Behat\Definition\Translator;
use Behat\Behat\Definition\Definition;
use Behat\Testwork\Suite\Suite;
use Symfony\Component\Translation\TranslatorInterface;
/**
* Translates definitions using translator component.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
final class DefinitionTranslator
{
/**
* @var TranslatorInterface
*/
private $translator;
/**
* Initialises definition translator.
*
* @param TranslatorInterface $translator
*/
public function __construct(TranslatorInterface $translator)
{
$this->translator = $translator;
}
/**
* Attempts to translate definition using translator and produce translated one on success.
*
* @param Suite $suite
* @param Definition $definition
* @param null|string $language
*
* @return Definition|TranslatedDefinition
*/
public function translateDefinition(Suite $suite, Definition $definition, $language = null)
{
$assetsId = $suite->getName();
$pattern = $definition->getPattern();
$translatedPattern = $this->translator->trans($pattern, array(), $assetsId, $language);
if ($pattern != $translatedPattern) {
return new TranslatedDefinition($definition, $translatedPattern, $language);
}
return $definition;
}
public function getLocale()
{
return $this->translator->getLocale();
}
}

View file

@ -0,0 +1,140 @@
<?php
/*
* This file is part of the Behat.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Behat\Behat\Definition\Translator;
use Behat\Behat\Definition\Definition;
/**
* Represents definition translated to the specific language.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
final class TranslatedDefinition implements Definition
{
/**
* @var Definition
*/
private $definition;
/**
* @var string
*/
private $translatedPattern;
/**
* @var string
*/
private $language;
/**
* Initialises translated definition.
*
* @param Definition $definition
* @param string $translatedPattern
* @param string $language
*/
public function __construct(Definition $definition, $translatedPattern, $language)
{
$this->definition = $definition;
$this->translatedPattern = $translatedPattern;
$this->language = $language;
}
/**
* {@inheritdoc}
*/
public function getType()
{
return $this->definition->getType();
}
/**
* {@inheritdoc}
*/
public function getPattern()
{
return $this->translatedPattern;
}
/**
* Returns original (not translated) pattern.
*
* @return string
*/
public function getOriginalPattern()
{
return $this->definition->getPattern();
}
/**
* Returns language definition was translated to.
*
* @return string
*/
public function getLanguage()
{
return $this->language;
}
/**
* {@inheritdoc}
*/
public function getDescription()
{
return $this->definition->getDescription();
}
/**
* {@inheritdoc}
*/
public function getPath()
{
return $this->definition->getPath();
}
/**
* {@inheritdoc}
*/
public function isAMethod()
{
return $this->definition->isAMethod();
}
/**
* {@inheritdoc}
*/
public function isAnInstanceMethod()
{
return $this->definition->isAnInstanceMethod();
}
/**
* {@inheritdoc}
*/
public function getCallable()
{
return $this->definition->getCallable();
}
/**
* {@inheritdoc}
*/
public function getReflection()
{
return $this->definition->getReflection();
}
/**
* {@inheritdoc}
*/
public function __toString()
{
return $this->definition->__toString();
}
}

View file

@ -0,0 +1,108 @@
<?php
/*
* This file is part of the Behat.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Behat\Behat\EventDispatcher\Cli;
use Behat\Behat\EventDispatcher\Event\AfterScenarioTested;
use Behat\Behat\EventDispatcher\Event\ExampleTested;
use Behat\Behat\EventDispatcher\Event\ScenarioTested;
use Behat\Testwork\Cli\Controller;
use Behat\Testwork\EventDispatcher\Event\AfterExerciseAborted;
use Behat\Testwork\EventDispatcher\Event\AfterSuiteAborted;
use Behat\Testwork\EventDispatcher\Event\ExerciseCompleted;
use Behat\Testwork\EventDispatcher\Event\SuiteTested;
use Behat\Testwork\Tester\Result\Interpretation\ResultInterpretation;
use Behat\Testwork\Tester\Result\Interpretation\SoftInterpretation;
use Behat\Testwork\Tester\Result\Interpretation\StrictInterpretation;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
/**
* Stops tests on first scenario failure.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
final class StopOnFailureController implements Controller
{
/**
* @var EventDispatcherInterface
*/
private $eventDispatcher;
/**
* @var ResultInterpretation
*/
private $resultInterpretation;
/**
* Initializes controller.
*
* @param EventDispatcherInterface $eventDispatcher
*/
public function __construct(EventDispatcherInterface $eventDispatcher)
{
$this->eventDispatcher = $eventDispatcher;
$this->resultInterpretation = new SoftInterpretation();
}
/**
* Configures command to be executable by the controller.
*
* @param Command $command
*/
public function configure(Command $command)
{
$command->addOption('--stop-on-failure', null, InputOption::VALUE_NONE,
'Stop processing on first failed scenario.'
);
}
/**
* Executes controller.
*
* @param InputInterface $input
* @param OutputInterface $output
*
* @return null|integer
*/
public function execute(InputInterface $input, OutputInterface $output)
{
if (!$input->getOption('stop-on-failure')) {
return null;
}
if ($input->getOption('strict')) {
$this->resultInterpretation = new StrictInterpretation();
}
$this->eventDispatcher->addListener(ScenarioTested::AFTER, array($this, 'exitOnFailure'), -100);
$this->eventDispatcher->addListener(ExampleTested::AFTER, array($this, 'exitOnFailure'), -100);
}
/**
* Exits if scenario is a failure and if stopper is enabled.
*
* @param AfterScenarioTested $event
*/
public function exitOnFailure(AfterScenarioTested $event)
{
if (!$this->resultInterpretation->isFailure($event->getTestResult())) {
return;
}
$this->eventDispatcher->dispatch(SuiteTested::AFTER, new AfterSuiteAborted($event->getEnvironment()));
$this->eventDispatcher->dispatch(ExerciseCompleted::AFTER, new AfterExerciseAborted());
exit(1);
}
}

View file

@ -0,0 +1,96 @@
<?php
/*
* This file is part of the Behat.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Behat\Behat\EventDispatcher\Event;
use Behat\Gherkin\Node\BackgroundNode;
use Behat\Gherkin\Node\FeatureNode;
use Behat\Gherkin\Node\ScenarioInterface;
use Behat\Testwork\Environment\Environment;
use Behat\Testwork\EventDispatcher\Event\AfterSetup;
use Behat\Testwork\Tester\Setup\Setup;
/**
* Represents an event right after background was setup for testing.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
final class AfterBackgroundSetup extends BackgroundTested implements AfterSetup
{
/**
* @var FeatureNode
*/
private $feature;
/**
* @var BackgroundNode
*/
private $background;
/**
* @var Setup
*/
private $setup;
/**
* Initializes event.
*
* @param Environment $env
* @param FeatureNode $feature
* @param BackgroundNode $background
* @param Setup $setup
*/
public function __construct(Environment $env, FeatureNode $feature, BackgroundNode $background, Setup $setup)
{
parent::__construct($env);
$this->feature = $feature;
$this->background = $background;
$this->setup = $setup;
}
/**
* Returns feature.
*
* @return FeatureNode
*/
public function getFeature()
{
return $this->feature;
}
/**
* Returns scenario node.
*
* @return ScenarioInterface
*/
public function getScenario()
{
return $this->background;
}
/**
* Returns background node.
*
* @return BackgroundNode
*/
public function getBackground()
{
return $this->background;
}
/**
* Returns current test setup.
*
* @return Setup
*/
public function getSetup()
{
return $this->setup;
}
}

View file

@ -0,0 +1,118 @@
<?php
/*
* This file is part of the Behat.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Behat\Behat\EventDispatcher\Event;
use Behat\Gherkin\Node\BackgroundNode;
use Behat\Gherkin\Node\FeatureNode;
use Behat\Gherkin\Node\ScenarioInterface;
use Behat\Testwork\Environment\Environment;
use Behat\Testwork\EventDispatcher\Event\AfterTested;
use Behat\Testwork\Tester\Result\TestResult;
use Behat\Testwork\Tester\Setup\Teardown;
/**
* Represents an event in which background was tested.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
final class AfterBackgroundTested extends BackgroundTested implements AfterTested
{
/**
* @var FeatureNode
*/
private $feature;
/**
* @var BackgroundNode
*/
private $background;
/**
* @var TestResult
*/
private $result;
/**
* @var Teardown
*/
private $teardown;
/**
* Initializes event.
*
* @param Environment $env
* @param FeatureNode $feature
* @param BackgroundNode $background
* @param TestResult $result
* @param Teardown $teardown
*/
public function __construct(
Environment $env,
FeatureNode $feature,
BackgroundNode $background,
TestResult $result,
Teardown $teardown
) {
parent::__construct($env);
$this->feature = $feature;
$this->background = $background;
$this->result = $result;
$this->teardown = $teardown;
}
/**
* Returns feature.
*
* @return FeatureNode
*/
public function getFeature()
{
return $this->feature;
}
/**
* Returns scenario node.
*
* @return ScenarioInterface
*/
public function getScenario()
{
return $this->background;
}
/**
* Returns background node.
*
* @return BackgroundNode
*/
public function getBackground()
{
return $this->background;
}
/**
* Returns current test result.
*
* @return TestResult
*/
public function getTestResult()
{
return $this->result;
}
/**
* Returns current test teardown.
*
* @return Teardown
*/
public function getTeardown()
{
return $this->teardown;
}
}

View file

@ -0,0 +1,68 @@
<?php
/*
* This file is part of the Behat.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Behat\Behat\EventDispatcher\Event;
use Behat\Gherkin\Node\FeatureNode;
use Behat\Testwork\Environment\Environment;
use Behat\Testwork\EventDispatcher\Event\AfterSetup;
use Behat\Testwork\Tester\Setup\Setup;
/**
* Represents an event right after feature is setup for a test.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
final class AfterFeatureSetup extends FeatureTested implements AfterSetup
{
/**
* @var FeatureNode
*/
private $feature;
/**
* @var Setup
*/
private $setup;
/**
* Initializes event.
*
* @param Environment $env
* @param FeatureNode $feature
* @param Setup $setup
*/
public function __construct(Environment $env, FeatureNode $feature, Setup $setup)
{
parent::__construct($env);
$this->feature = $feature;
$this->setup = $setup;
}
/**
* Returns feature.
*
* @return FeatureNode
*/
public function getFeature()
{
return $this->feature;
}
/**
* Returns current test setup.
*
* @return Setup
*/
public function getSetup()
{
return $this->setup;
}
}

View file

@ -0,0 +1,85 @@
<?php
/*
* This file is part of the Behat.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Behat\Behat\EventDispatcher\Event;
use Behat\Gherkin\Node\FeatureNode;
use Behat\Testwork\Environment\Environment;
use Behat\Testwork\EventDispatcher\Event\AfterTested;
use Behat\Testwork\Tester\Result\TestResult;
use Behat\Testwork\Tester\Setup\Teardown;
/**
* Represents an event right after feature was tested.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
final class AfterFeatureTested extends FeatureTested implements AfterTested
{
/**
* @var FeatureNode
*/
private $feature;
/**
* @var TestResult
*/
private $result;
/**
* @var Teardown
*/
private $teardown;
/**
* Initializes event.
*
* @param Environment $env
* @param FeatureNode $feature
* @param TestResult $result
* @param Teardown $teardown
*/
public function __construct(Environment $env, FeatureNode $feature, TestResult $result, Teardown $teardown)
{
parent::__construct($env);
$this->feature = $feature;
$this->result = $result;
$this->teardown = $teardown;
}
/**
* Returns feature.
*
* @return FeatureNode
*/
public function getFeature()
{
return $this->feature;
}
/**
* Returns current test result.
*
* @return TestResult
*/
public function getTestResult()
{
return $this->result;
}
/**
* Returns current test teardown.
*
* @return Teardown
*/
public function getTeardown()
{
return $this->teardown;
}
}

View file

@ -0,0 +1,85 @@
<?php
/*
* This file is part of the Behat.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Behat\Behat\EventDispatcher\Event;
use Behat\Gherkin\Node\FeatureNode;
use Behat\Gherkin\Node\OutlineNode;
use Behat\Testwork\Environment\Environment;
use Behat\Testwork\EventDispatcher\Event\AfterSetup;
use Behat\Testwork\Tester\Setup\Setup;
/**
* Represents an event right after outline setup.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
final class AfterOutlineSetup extends OutlineTested implements AfterSetup
{
/**
* @var FeatureNode
*/
private $feature;
/**
* @var OutlineNode
*/
private $outline;
/**
* @var Setup
*/
private $setup;
/**
* Initializes event.
*
* @param Environment $env
* @param FeatureNode $feature
* @param OutlineNode $outline
* @param Setup $setup
*/
public function __construct(Environment $env, FeatureNode $feature, OutlineNode $outline, Setup $setup)
{
parent::__construct($env);
$this->feature = $feature;
$this->outline = $outline;
$this->setup = $setup;
}
/**
* Returns feature.
*
* @return FeatureNode
*/
public function getFeature()
{
return $this->feature;
}
/**
* Returns outline node.
*
* @return OutlineNode
*/
public function getOutline()
{
return $this->outline;
}
/**
* Returns current test setup.
*
* @return Setup
*/
public function getSetup()
{
return $this->setup;
}
}

View file

@ -0,0 +1,107 @@
<?php
/*
* This file is part of the Behat.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Behat\Behat\EventDispatcher\Event;
use Behat\Gherkin\Node\FeatureNode;
use Behat\Gherkin\Node\OutlineNode;
use Behat\Testwork\Environment\Environment;
use Behat\Testwork\EventDispatcher\Event\AfterTested;
use Behat\Testwork\Tester\Result\TestResult;
use Behat\Testwork\Tester\Setup\Teardown;
/**
* Represents an event after outline was tested.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
final class AfterOutlineTested extends OutlineTested implements AfterTested
{
/**
* @var FeatureNode
*/
private $feature;
/**
* @var OutlineNode
*/
private $outline;
/**
* @var TestResult
*/
private $result;
/**
* @var Teardown
*/
private $teardown;
/**
* Initializes event.
*
* @param Environment $env
* @param FeatureNode $feature
* @param OutlineNode $outline
* @param TestResult $result
* @param Teardown $teardown
*/
public function __construct(
Environment $env,
FeatureNode $feature,
OutlineNode $outline,
TestResult $result,
Teardown $teardown
) {
parent::__construct($env);
$this->feature = $feature;
$this->outline = $outline;
$this->result = $result;
$this->teardown = $teardown;
}
/**
* Returns feature.
*
* @return FeatureNode
*/
public function getFeature()
{
return $this->feature;
}
/**
* Returns outline node.
*
* @return OutlineNode
*/
public function getOutline()
{
return $this->outline;
}
/**
* Returns current test result.
*
* @return TestResult
*/
public function getTestResult()
{
return $this->result;
}
/**
* Returns current test teardown.
*
* @return Teardown
*/
public function getTeardown()
{
return $this->teardown;
}
}

View file

@ -0,0 +1,86 @@
<?php
/*
* This file is part of the Behat.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Behat\Behat\EventDispatcher\Event;
use Behat\Gherkin\Node\FeatureNode;
use Behat\Gherkin\Node\ScenarioLikeInterface as Scenario;
use Behat\Gherkin\Node\ScenarioNode;
use Behat\Testwork\Environment\Environment;
use Behat\Testwork\EventDispatcher\Event\AfterSetup;
use Behat\Testwork\Tester\Setup\Setup;
/**
* Represents an event after scenario setup.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
final class AfterScenarioSetup extends ScenarioTested implements AfterSetup
{
/**
* @var FeatureNode
*/
private $feature;
/**
* @var Scenario
*/
private $scenario;
/**
* @var Setup
*/
private $setup;
/**
* Initializes event
*
* @param Environment $env
* @param FeatureNode $feature
* @param Scenario $scenario
* @param Setup $setup
*/
public function __construct(Environment $env, FeatureNode $feature, Scenario $scenario, Setup $setup)
{
parent::__construct($env);
$this->feature = $feature;
$this->scenario = $scenario;
$this->setup = $setup;
}
/**
* Returns feature.
*
* @return FeatureNode
*/
public function getFeature()
{
return $this->feature;
}
/**
* Returns scenario node.
*
* @return ScenarioNode
*/
public function getScenario()
{
return $this->scenario;
}
/**
* Returns current test setup.
*
* @return Setup
*/
public function getSetup()
{
return $this->setup;
}
}

View file

@ -0,0 +1,108 @@
<?php
/*
* This file is part of the Behat.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Behat\Behat\EventDispatcher\Event;
use Behat\Gherkin\Node\FeatureNode;
use Behat\Gherkin\Node\ScenarioLikeInterface as Scenario;
use Behat\Gherkin\Node\ScenarioNode;
use Behat\Testwork\Environment\Environment;
use Behat\Testwork\EventDispatcher\Event\AfterTested;
use Behat\Testwork\Tester\Result\TestResult;
use Behat\Testwork\Tester\Setup\Teardown;
/**
* Represents an event after scenario has been tested.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
final class AfterScenarioTested extends ScenarioTested implements AfterTested
{
/**
* @var FeatureNode
*/
private $feature;
/**
* @var Scenario
*/
private $scenario;
/**
* @var TestResult
*/
private $result;
/**
* @var Teardown
*/
private $teardown;
/**
* Initializes event
*
* @param Environment $env
* @param FeatureNode $feature
* @param Scenario $scenario
* @param TestResult $result
* @param Teardown $teardown
*/
public function __construct(
Environment $env,
FeatureNode $feature,
Scenario $scenario,
TestResult $result,
Teardown $teardown
) {
parent::__construct($env);
$this->feature = $feature;
$this->scenario = $scenario;
$this->result = $result;
$this->teardown = $teardown;
}
/**
* Returns feature.
*
* @return FeatureNode
*/
public function getFeature()
{
return $this->feature;
}
/**
* Returns scenario node.
*
* @return ScenarioNode
*/
public function getScenario()
{
return $this->scenario;
}
/**
* Returns current test result.
*
* @return TestResult
*/
public function getTestResult()
{
return $this->result;
}
/**
* Returns current test teardown.
*
* @return Teardown
*/
public function getTeardown()
{
return $this->teardown;
}
}

Some files were not shown because too many files have changed in this diff Show more