protected function get_mobile_version_tags($verbose = true) : string {
        global $CFG;

        if (!empty($CFG->behat_ionic_dirroot)) {
            // Get app version from package.json.
            $jsonpath = $CFG->behat_ionic_dirroot . '/package.json';
            $json = @file_get_contents($jsonpath);
            if (!$json) {
                throw new coding_exception('Unable to load app version from ' . $jsonpath);
            }
            $package = json_decode($json);
            if ($package === null || empty($package->version)) {
                throw new coding_exception('Invalid app package data in ' . $jsonpath);
            }
            $installedversion = $package->version;
        } else if (!empty($CFG->behat_ionic_wwwroot)) {
            // Get app version from config.json inside wwwroot.
            $jsonurl = $CFG->behat_ionic_wwwroot . '/config.json';
            $json = @download_file_content($jsonurl);
            if (!$json) {
                throw new coding_exception('Unable to load app version from ' . $jsonurl);
            }
            $config = json_decode($json);
            if ($config === null || empty($config->versionname)) {
                throw new coding_exception('Invalid app config data in ' . $jsonurl);
            }
            $installedversion = str_replace('-dev', '', $config->versionname);
        } else {
            return '';
        }

        // Read all feature files to check which mobile tags are used. (Note: This could be cached
        // but ideally, it is the sort of thing that really ought to be refreshed by doing a new
        // Behat init. Also, at time of coding it only takes 0.3 seconds and only if app enabled.)
        $usedtags = [];
        foreach ($this->features as $filepath) {
            $feature = file_get_contents($filepath);
            // This may incorrectly detect versions used e.g. in a comment or something, but it
            // doesn't do much harm if we have extra ones.
            if (preg_match_all('~@app_(?:from|upto)(?:[0-9]+(?:\.[0-9]+)*)~', $feature, $matches)) {
                foreach ($matches[0] as $tag) {
                    // Store as key in array so we don't get duplicates.
                    $usedtags[$tag] = true;
                }
            }
        }

        // Set up relevant tags for each version.
        $tags = [];
        foreach ($usedtags as $usedtag => $ignored) {
            if (!preg_match('~^@app_(from|upto)([0-9]+(?:\.[0-9]+)*)$~', $usedtag, $matches)) {
                throw new coding_exception('Unexpected tag format');
            }
            $direction = $matches[1];
            $version = $matches[2];

            switch (version_compare($installedversion, $version)) {
                case -1:
                    // Installed version OLDER than the one being considered, so do not
                    // include any scenarios that only run from the considered version up.
                    if ($direction === 'from') {
                        $tags[] = '~@app_from' . $version;
                    }
                    break;

                case 0:
                    // Installed version EQUAL to the one being considered - no tags need
                    // excluding.
                    break;

                case 1:
                    // Installed version NEWER than the one being considered, so do not
                    // include any scenarios that only run up to that version.
                    if ($direction === 'upto') {
                        $tags[] = '~@app_upto' . $version;
                    }
                    break;
            }
        }

        if ($verbose) {
            mtrace('Configured app tests for version ' . $installedversion);
        }

        return join(' && ', $tags);
    }