Commit 91db25eb authored by catch's avatar catch

Issue #2171683 by sun, tstoeckler, larowlan: Remove all Simpletest overrides...

Issue #2171683 by sun, tstoeckler, larowlan: Remove all Simpletest overrides and rely on native multi-site functionality instead.
parent 1b75d423
......@@ -240,15 +240,15 @@
* @see default.settings.php
*/
function conf_path($require_settings = TRUE, $reset = FALSE) {
$conf_path = &drupal_static(__FUNCTION__, '');
static $conf_path;
if ($conf_path && !$reset) {
if (isset($conf_path) && !$reset) {
return $conf_path;
}
// Check for a simpletest override.
if ($simpletest_conf_path = _drupal_simpletest_conf_path()) {
$conf_path = $simpletest_conf_path;
if ($test_prefix = drupal_valid_test_ua()) {
$conf_path = 'sites/simpletest/' . substr($test_prefix, 10);
return $conf_path;
}
......@@ -262,50 +262,6 @@ function conf_path($require_settings = TRUE, $reset = FALSE) {
return $conf_path;
}
/**
* Determines whether to use an overridden value for conf_path().
*
* Simpletest may provide a secondary, test-specific settings.php file to load
* after the primary one used by the parent site and override its variables.
* - If the child settings.php does not override $conf_path, then this function
* returns FALSE and conf_path() returns the directory of the primary
* settings.php.
* - If the child settings.php does override $conf_path, then
* _drupal_load_test_overrides() sets the 'simpletest_conf_path' setting, and
* this function returns that to conf_path(), causing installations and
* upgrades to act on that one.
*
* @return string|false
* The overridden $conf_path, or FALSE if the $conf_path should not currently
* be overridden.
*
* @see conf_path()
* @see _drupal_load_test_overrides()
*/
function _drupal_simpletest_conf_path() {
// Ensure that the settings object is available. conf_path() is called once
// before the Settings class is included, and at that point it should still
// load the primary $conf_path. See drupal_settings_initialize().
if (!class_exists('Drupal\Component\Utility\Settings', FALSE)) {
return FALSE;
}
// If no $simpletest_conf_path is set, use the normal $conf_path.
if (!($simpletest_conf_path = settings()->get('simpletest_conf_path'))) {
return FALSE;
}
// Ensure that this is actually a simpletest request. We can't check this
// before settings.php is loaded.
if (!drupal_valid_test_ua()) {
return FALSE;
}
// When the $simpletest_conf_path is set in a valid test request,
// return that path.
return $simpletest_conf_path;
}
/**
* Finds the appropriate configuration directory for a given host and path.
*
......@@ -534,20 +490,32 @@ function drupal_valid_http_host($host) {
* Sets the base URL, cookie domain, and session name from configuration.
*/
function drupal_settings_initialize() {
global $base_url, $base_path, $base_root, $script_path;
// Export these settings.php variables to the global namespace.
global $databases, $cookie_domain, $db_prefix, $drupal_hash_salt, $base_secure_url, $base_insecure_url, $config_directories, $config;
global $base_url, $databases, $cookie_domain, $drupal_hash_salt, $config_directories, $config;
$settings = array();
$config = array();
// Make conf_path() available as local variable in settings.php.
$conf_path = conf_path();
if (is_readable(DRUPAL_ROOT . '/' . $conf_path . '/settings.php')) {
include_once DRUPAL_ROOT . '/' . $conf_path . '/settings.php';
require DRUPAL_ROOT . '/' . $conf_path . '/settings.php';
}
// Initialize Settings.
new Settings($settings);
}
/**
* Initializes global request variables.
*
* @todo D8: Eliminate this entirely in favor of Request object.
*/
function _drupal_request_initialize() {
// Provided by settings.php.
// @see drupal_settings_initialize()
global $base_url, $cookie_domain;
// Set and derived from $base_url by this function.
global $base_path, $base_root, $script_path;
global $base_secure_url, $base_insecure_url;
$is_https = isset($_SERVER['HTTPS']) && strtolower($_SERVER['HTTPS']) == 'on';
......@@ -1753,11 +1721,27 @@ function _drupal_exception_handler($exception) {
*/
function _drupal_bootstrap_configuration() {
drupal_environment_initialize();
// Indicate that code is operating in a test child site.
if ($test_prefix = drupal_valid_test_ua()) {
// Only code that interfaces directly with tests should rely on this
// constant; e.g., the error/exception handler conditionally adds further
// error information into HTTP response headers that are consumed by
// Simpletest's internal browser.
define('DRUPAL_TEST_IN_CHILD_SITE', TRUE);
// Log fatal errors to the test site directory.
ini_set('log_errors', 1);
ini_set('error_log', DRUPAL_ROOT . '/sites/simpletest/' . substr($test_prefix, 10) . '/error.log');
}
else {
// Ensure that no other code defines this.
define('DRUPAL_TEST_IN_CHILD_SITE', FALSE);
}
// Initialize the configuration, including variables from settings.php.
drupal_settings_initialize();
// Make sure we are using the test database prefix in child Drupal sites.
_drupal_initialize_db_test_prefix();
_drupal_request_initialize();
// Activate the class loader.
drupal_classloader();
......@@ -1845,39 +1829,6 @@ function _drupal_bootstrap_page_cache() {
}
}
/**
* In a test environment, get the test db prefix and set it in $databases.
*/
function _drupal_initialize_db_test_prefix() {
// The user agent header is used to pass a database prefix in the request when
// running tests. However, for security reasons, it is imperative that we
// validate we ourselves made the request.
if ($test_prefix = drupal_valid_test_ua()) {
// Set the test run id for use in other parts of Drupal.
$test_info = &$GLOBALS['drupal_test_info'];
$test_info['test_run_id'] = $test_prefix;
$test_info['in_child_site'] = TRUE;
foreach ($GLOBALS['databases']['default'] as &$value) {
// Extract the current default database prefix.
if (!isset($value['prefix'])) {
$current_prefix = '';
}
elseif (is_array($value['prefix'])) {
$current_prefix = $value['prefix']['default'];
}
else {
$current_prefix = $value['prefix'];
}
// Remove the current database prefix and replace it by our own.
$value['prefix'] = array(
'default' => $current_prefix . $test_prefix,
);
}
}
}
/**
* Returns the current bootstrap phase for this Drupal process.
*
......@@ -1992,10 +1943,9 @@ function module_hook($module, $hook) {
* Returns the test prefix if this is an internal request from SimpleTest.
*
* @param string $new_prefix
* Internal use only. A new prefix to be stored. Passed in by tests that use
* the test runner from within a test.
* Internal use only. A new prefix to be stored.
*
* @return
* @return string|FALSE
* Either the simpletest prefix (the string "simpletest" followed by any
* number of digits) or FALSE if the user agent does not contain a valid
* HMAC and timestamp.
......@@ -2009,82 +1959,68 @@ function drupal_valid_test_ua($new_prefix = NULL) {
if (isset($test_prefix)) {
return $test_prefix;
}
// Unless the below User-Agent and HMAC validation succeeds, we are not in
// a test environment.
$test_prefix = FALSE;
// Perform a basic check on the User-Agent HTTP request header first. Any
// inbound request that uses the simpletest UA header needs to be validated.
if (isset($_SERVER['HTTP_USER_AGENT']) && preg_match("/^(simpletest\d+);(.+);(.+);(.+)$/", $_SERVER['HTTP_USER_AGENT'], $matches)) {
list(, $prefix, $time, $salt, $hmac) = $matches;
$check_string = $prefix . ';' . $time . ';' . $salt;
// We use the salt from settings.php to make the HMAC key, since
// the database is not yet initialized and we can't access the configuration
// system. The file properties add more entropy not easily accessible to
// others.
$key = drupal_get_hash_salt() . filectime(__FILE__) . fileinode(__FILE__);
// Read the hash salt prepared by drupal_generate_test_ua().
// This function is called before settings.php is read and Drupal's error
// handlers are set up. While Drupal's error handling may be properly
// configured on production sites, the server's PHP error_reporting may not.
// Ensure that no information leaks on production sites.
$key_file = DRUPAL_ROOT . '/sites/simpletest/' . substr($prefix, 10) . '/.htkey';
if (!is_readable($key_file)) {
header($_SERVER['SERVER_PROTOCOL'] . ' 403 Forbidden');
exit;
}
$private_key = file_get_contents($key_file);
// The file properties add more entropy not easily accessible to others.
$key = $private_key . filectime(__FILE__) . fileinode(__FILE__);
$time_diff = REQUEST_TIME - $time;
// We can't use Crypt::hmacBase64() yet because this can be called in very
// early bootstrap when autoloader has not been initialized yet.
$test_hmac = base64_encode(hash_hmac('sha256', $check_string, $key, TRUE));
$test_hmac = strtr($test_hmac, array('+' => '-', '/' => '_', '=' => ''));
$test_hmac = Crypt::hmacBase64($check_string, $key);
// Since we are making a local request a 5 second time window is allowed,
// and the HMAC must match.
if ($time_diff >= 0 && $time_diff <= 5 && $hmac == $test_hmac) {
if ($time_diff >= 0 && $time_diff <= 5 && $hmac === $test_hmac) {
$test_prefix = $prefix;
_drupal_load_test_overrides($test_prefix);
return $test_prefix;
}
}
$test_prefix = FALSE;
return $test_prefix;
}
/**
* Overrides low-level and environment-specific configuration.
*
* Very strictly for internal use only.
*
* Loads settings.php from the simpletest public files directory. These files
* can change the global $config_directories, the return value of conf_path(),
* settings(), and $config overrides.
*
* @param string $test_prefix
* The simpletest prefix.
*/
function _drupal_load_test_overrides($test_prefix) {
global $config_directories, $config;
// Do not use the parent site's config directories. Use only the child site's.
// @see \Drupal\simpletest\TestBase::prepareConfigDirectories()
$path_prefix = 'simpletest/' . substr($test_prefix, 10);
$config_directories = array();
foreach (array(CONFIG_ACTIVE_DIRECTORY, CONFIG_STAGING_DIRECTORY) as $type) {
$config_directories[$type] = conf_path() . '/files/' . $path_prefix . '/config_' . $type;
}
// Check for and load a settings.php file in the simpletest files directory.
$filename = conf_path() . '/files/' . $path_prefix . '/settings.php';
if (file_exists($filename)) {
$settings = settings()->getAll();
$conf_path = &drupal_static('conf_path');
// This can override $config, $conf_path, $settings, and $config_directories.
include $filename;
// Keep the overriden $conf_path alive across drupal_static_reset() calls.
// @see conf_path()
$settings['simpletest_conf_path'] = $conf_path;
new Settings($settings);
}
}
/**
* Generates a user agent string with a HMAC and timestamp for simpletest.
*/
function drupal_generate_test_ua($prefix) {
static $key;
if (!isset($key)) {
// We use the salt from settings.php to make the HMAC key, since
// the database is not yet initialized and we can't access the configuration
// system. The file properties add more entropy not easily accessible to
// others.
$key = drupal_get_hash_salt() . filectime(__FILE__) . fileinode(__FILE__);
static $key, $last_prefix;
if (!isset($key) || $last_prefix != $prefix) {
$last_prefix = $prefix;
$key_file = DRUPAL_ROOT . '/sites/simpletest/' . substr($prefix, 10) . '/.htkey';
// When issuing an outbound HTTP client request from within an inbound test
// request, then the outbound request has to use the same User-Agent header
// as the inbound request. A newly generated private key for the same test
// prefix would invalidate all subsequent inbound requests.
// @see \Drupal\Core\Http\Plugin\SimpletestHttpRequestSubscriber
if (DRUPAL_TEST_IN_CHILD_SITE && $parent_prefix = drupal_valid_test_ua()) {
if ($parent_prefix != $prefix) {
throw new \RuntimeException("Malformed User-Agent: Expected '$parent_prefix' but got '$prefix'.");
}
// If the file is not readable, a PHP warning is expected in this case.
$private_key = file_get_contents($key_file);
}
else {
// Generate and save a new hash salt for a test run.
// Consumed by drupal_valid_test_ua() before settings.php is loaded.
$private_key = Crypt::randomStringHashed(55);
file_put_contents($key_file, $private_key);
}
// The file properties add more entropy not easily accessible to others.
$key = $private_key . filectime(__FILE__) . fileinode(__FILE__);
}
// Generate a moderately secure HMAC based on the database credentials.
$salt = uniqid('', TRUE);
......
......@@ -3071,14 +3071,6 @@ function _drupal_bootstrap_code() {
// Make sure all stream wrappers are registered.
file_get_stream_wrappers();
// Now that stream wrappers are registered, log fatal errors from a simpletest
// child site to a test specific file directory.
$test_info = &$GLOBALS['drupal_test_info'];
if (!empty($test_info['in_child_site'])) {
ini_set('log_errors', 1);
ini_set('error_log', 'public://error.log');
}
// Set the allowed protocols once we have the config available.
$allowed_protocols = \Drupal::config('system.filter')->get('protocols');
if (!isset($allowed_protocols)) {
......
......@@ -141,8 +141,7 @@ function _drupal_log_error($error, $fatal = FALSE) {
// When running inside the testing framework, we relay the errors
// to the tested site by the way of HTTP headers.
$test_info = &$GLOBALS['drupal_test_info'];
if (!empty($test_info['in_child_site']) && !headers_sent() && (!defined('SIMPLETEST_COLLECT_ERRORS') || SIMPLETEST_COLLECT_ERRORS)) {
if (DRUPAL_TEST_IN_CHILD_SITE && !headers_sent() && (!defined('SIMPLETEST_COLLECT_ERRORS') || SIMPLETEST_COLLECT_ERRORS)) {
// $number does not use drupal_static as it should not be reset
// as it uniquely identifies each PHP error.
static $number = 0;
......
......@@ -276,19 +276,18 @@ function install_begin_request(&$install_state) {
// which will be used for installing Drupal.
conf_path(FALSE);
drupal_bootstrap(DRUPAL_BOOTSTRAP_CONFIGURATION);
// If the hash salt leaks, it becomes possible to forge a valid testing user
// agent, install a new copy of Drupal, and take over the original site. To
// avoid this yet allow for automated testing of the installer, make sure
// there is also a special test-specific settings.php overriding conf_path().
// _drupal_load_test_overrides() sets the simpletest_conf_path in-memory
// setting in this case.
if ($install_state['interactive'] && drupal_valid_test_ua() && !settings()->get('simpletest_conf_path')) {
// agent, install a new copy of Drupal, and take over the original site.
// The user agent header is used to pass a database prefix in the request when
// running tests. However, for security reasons, it is imperative that no
// installation be permitted using such a prefix.
if ($install_state['interactive'] && strpos($request->server->get('HTTP_USER_AGENT'), 'simpletest') !== FALSE && !drupal_valid_test_ua()) {
header($request->server->get('SERVER_PROTOCOL') . ' 403 Forbidden');
exit;
}
drupal_bootstrap(DRUPAL_BOOTSTRAP_CONFIGURATION);
// Ensure that procedural dependencies are loaded as early as possible,
// since the error/exception handlers depend on them.
require_once __DIR__ . '/../modules/system/system.install';
......@@ -1080,7 +1079,6 @@ function install_verify_database_settings() {
global $databases;
if (!empty($databases)) {
$database = $databases['default']['default'];
drupal_static_reset('conf_path');
$settings_file = './' . conf_path(FALSE) . '/settings.php';
$errors = install_database_errors($database, $settings_file);
if (empty($errors)) {
......@@ -1103,7 +1101,6 @@ function install_verify_database_settings() {
function install_settings_form($form, &$form_state, &$install_state) {
global $databases;
drupal_static_reset('conf_path');
$conf_path = './' . conf_path(FALSE);
$settings_file = $conf_path . '/settings.php';
$database = isset($databases['default']['default']) ? $databases['default']['default'] : array();
......@@ -1165,12 +1162,6 @@ function install_settings_form($form, &$form_state, &$install_state) {
function install_settings_form_validate($form, &$form_state) {
$driver = $form_state['values']['driver'];
$database = $form_state['values'][$driver];
// When testing the interactive installer, copy the database password and
// the test prefix.
if ($test_prefix = drupal_valid_test_ua()) {
$database['prefix'] = $test_prefix;
$database['password'] = $GLOBALS['databases']['default']['default']['password'];
}
$drivers = drupal_get_database_types();
$reflection = new \ReflectionClass($drivers[$driver]);
$install_namespace = $reflection->getNamespaceName();
......@@ -1237,33 +1228,14 @@ function install_settings_form_submit($form, &$form_state) {
// Update global settings array and save.
$settings = array();
$database = $form_state['storage']['database'];
// Ideally, there is no difference between the code executed by the
// automated test browser and an ordinary browser. However, the database
// settings need a different format and also need to skip the password
// when testing. The hash salt also needs to be skipped because the original
// salt is used to verify the validity of the automated test browser.
// Because of these, there's a little difference in the code following but
// it is small and self-contained.
if ($test_prefix = drupal_valid_test_ua()) {
foreach ($form_state['storage']['database'] as $k => $v) {
if ($k != 'password') {
$settings['databases']['default']['default'][$k] = (object) array(
'value' => $v,
'required' => TRUE,
);
}
}
}
else {
$settings['databases']['default']['default'] = (object) array(
'value' => $database,
'required' => TRUE,
);
$settings['drupal_hash_salt'] = (object) array(
'value' => Crypt::randomStringHashed(55),
'required' => TRUE,
);
}
$settings['databases']['default']['default'] = (object) array(
'value' => $database,
'required' => TRUE,
);
$settings['drupal_hash_salt'] = (object) array(
'value' => Crypt::randomStringHashed(55),
'required' => TRUE,
);
// Remember the profile which was used.
$settings['settings'] = array(
......@@ -2381,7 +2353,7 @@ function install_check_requirements($install_state) {
if (!$install_state['settings_verified']) {
$readable = FALSE;
$writable = FALSE;
$conf_path = './' . conf_path(FALSE, TRUE);
$conf_path = './' . conf_path(FALSE);
$settings_file = $conf_path . '/settings.php';
$default_settings_file = './sites/default/default.settings.php';
$file = $conf_path;
......
......@@ -280,8 +280,7 @@ public function on500Html(FlattenException $exception, Request $request) {
// When running inside the testing framework, we relay the errors
// to the tested site by the way of HTTP headers.
$test_info = &$GLOBALS['drupal_test_info'];
if (!empty($test_info['in_child_site']) && !headers_sent() && (!defined('SIMPLETEST_COLLECT_ERRORS') || SIMPLETEST_COLLECT_ERRORS)) {
if (DRUPAL_TEST_IN_CHILD_SITE && !headers_sent() && (!defined('SIMPLETEST_COLLECT_ERRORS') || SIMPLETEST_COLLECT_ERRORS)) {
// $number does not use drupal_static as it should not be reset
// as it uniquely identifies each PHP error.
static $number = 0;
......
......@@ -49,7 +49,7 @@ public function getFormOptions(array $database) {
// Make the text more accurate for SQLite.
$form['database']['#title'] = t('Database file');
$form['database']['#description'] = t('The absolute path to the file where @drupal data will be stored. This must be writable by the web server and should exist outside of the web root.', array('@drupal' => drupal_install_profile_distribution_name()));
$default_database = conf_path(FALSE, TRUE) . '/files/.ht.sqlite';
$default_database = conf_path(FALSE) . '/files/.ht.sqlite';
$form['database']['#default_value'] = empty($database['database']) ? $default_database : $database['database'];
return $form;
}
......
......@@ -335,20 +335,13 @@ public function updateModules(array $module_list, array $module_filenames = arra
}
/**
* Returns the classname based on environment and testing prefix.
* Returns the classname based on environment.
*
* @return string
* The class name.
*/
protected function getClassName() {
$parts = array('service_container', $this->environment);
// Make sure to use a testing-specific container even in the parent site.
if (!empty($GLOBALS['drupal_test_info']['test_run_id'])) {
$parts[] = $GLOBALS['drupal_test_info']['test_run_id'];
}
elseif ($prefix = drupal_valid_test_ua()) {
$parts[] = $prefix;
}
return implode('_', $parts);
}
......
......@@ -34,9 +34,8 @@ public function onBeforeSendRequest(Event $event) {
// user-agent is used to ensure that multiple testing sessions running at the
// same time won't interfere with each other as they would if the database
// prefix were stored statically in a file or database variable.
$test_info = &$GLOBALS['drupal_test_info'];
if (!empty($test_info['test_run_id'])) {
$event['request']->setHeader('User-Agent', drupal_generate_test_ua($test_info['test_run_id']));
if ($test_prefix = drupal_valid_test_ua()) {
$event['request']->setHeader('User-Agent', drupal_generate_test_ua($test_prefix));
}
}
}
......@@ -38,13 +38,6 @@ public function getExternalUrl() {
*/
public static function basePath() {
$base_path = settings()->get('file_public_path', conf_path() . '/files');
if ($test_prefix = drupal_valid_test_ua()) {
// Append the testing suffix unless already given.
// @see \Drupal\simpletest\WebTestBase::setUp()
if (strpos($base_path, '/simpletest/' . substr($test_prefix, 10)) === FALSE) {
return $base_path . '/simpletest/' . substr($test_prefix, 10);
}
}
return $base_path;
}
......
......@@ -95,11 +95,12 @@ function testImportCreate() {
// Add the new files to the staging directory.
$src_dir = drupal_get_path('module', 'field_test_config') . '/staging';
$this->assertTrue(file_unmanaged_copy("$src_dir/$field_config_name.yml", "public://config_staging/$field_config_name.yml"));
$this->assertTrue(file_unmanaged_copy("$src_dir/$instance_config_name.yml", "public://config_staging/$instance_config_name.yml"));
$this->assertTrue(file_unmanaged_copy("$src_dir/$field_config_name_2.yml", "public://config_staging/$field_config_name_2.yml"));
$this->assertTrue(file_unmanaged_copy("$src_dir/$instance_config_name_2a.yml", "public://config_staging/$instance_config_name_2a.yml"));
$this->assertTrue(file_unmanaged_copy("$src_dir/$instance_config_name_2b.yml", "public://config_staging/$instance_config_name_2b.yml"));
$target_dir = $this->configDirectories[CONFIG_STAGING_DIRECTORY];
$this->assertTrue(file_unmanaged_copy("$src_dir/$field_config_name.yml", "$target_dir/$field_config_name.yml"));
$this->assertTrue(file_unmanaged_copy("$src_dir/$instance_config_name.yml", "$target_dir/$instance_config_name.yml"));
$this->assertTrue(file_unmanaged_copy("$src_dir/$field_config_name_2.yml", "$target_dir/$field_config_name_2.yml"));
$this->assertTrue(file_unmanaged_copy("$src_dir/$instance_config_name_2a.yml", "$target_dir/$instance_config_name_2a.yml"));
$this->assertTrue(file_unmanaged_copy("$src_dir/$instance_config_name_2b.yml", "$target_dir/$instance_config_name_2b.yml"));
// Import the content of the staging directory.
$this->configImporter()->import();
......
......@@ -16,7 +16,7 @@
*/
class DummyReadOnlyStreamWrapper extends LocalReadOnlyStream {
function getDirectoryPath() {
return 'sites/default/files';
return conf_path() . '/files';
}
/**
......
......@@ -16,7 +16,7 @@
*/
class DummyStreamWrapper extends LocalStream {
function getDirectoryPath() {
return 'sites/default/files';
return conf_path() . '/files';
}
/**
......
......@@ -23,13 +23,6 @@ class LanguageNegotiationInfoTest extends WebTestBase {
*/
public static $modules = array('language');
/**
* The language manager.
*
* @var \Drupal\language\ConfigurableLanguageManagerInterface
*/
protected $languageManager;
/**
* {@inheritdoc}
*/
......@@ -46,31 +39,60 @@ public static function getInfo() {
*/
function setUp() {
parent::setUp();
$this->languageManager = $this->container->get('language_manager');
$admin_user = $this->drupalCreateUser(array('administer languages', 'access administration pages', 'view the administration theme'));
$this->drupalLogin($admin_user);
$this->drupalPostForm('admin/config/regional/language/add', array('predefined_langcode' => 'it'), t('Add language'));
}
/**
* Returns the configurable language manager.
*
* @return \Drupal\language\ConfigurableLanguageManager
*/
protected function languageManager() {
return $this->container->get('language_manager');
}
/**
* Sets state flags for language_test module.
*
* Ensures to correctly update data both in the child site and the test runner
* environment.
*
* @param array $values
* The key/value pairs to set in state.
*/
protected function stateSet(array $values) {
// Set the new state values.
$this->container->get('state')->setMultiple($values);
// Refresh in-memory static state/config caches and static variables.
$this->refreshVariables();
// Refresh/rewrite language negotiation configuration, in order to pick up
// the manipulations performed by language_test module's info alter hooks.
$this->container->get('language_negotiator')->purgeConfiguration();
}
/**
* Tests alterations to language types/negotiation info.
*/
function testInfoAlterations() {