// Copyright 2014 The Kyua Authors.1// All rights reserved.2//3// Redistribution and use in source and binary forms, with or without4// modification, are permitted provided that the following conditions are5// met:6//7// * Redistributions of source code must retain the above copyright8// notice, this list of conditions and the following disclaimer.9// * Redistributions in binary form must reproduce the above copyright10// notice, this list of conditions and the following disclaimer in the11// documentation and/or other materials provided with the distribution.12// * Neither the name of Google Inc. nor the names of its contributors13// may be used to endorse or promote products derived from this software14// without specific prior written permission.15//16// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS17// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT18// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR19// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT20// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,21// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT22// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,23// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY24// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT25// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE26// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.2728#include "store/layout.hpp"2930#include <algorithm>31#include <cstring>3233#include "store/exceptions.hpp"34#include "utils/datetime.hpp"35#include "utils/format/macros.hpp"36#include "utils/fs/directory.hpp"37#include "utils/fs/exceptions.hpp"38#include "utils/fs/path.hpp"39#include "utils/fs/operations.hpp"40#include "utils/logging/macros.hpp"41#include "utils/env.hpp"42#include "utils/optional.ipp"43#include "utils/sanity.hpp"44#include "utils/text/exceptions.hpp"45#include "utils/text/regex.hpp"4647namespace datetime = utils::datetime;48namespace fs = utils::fs;49namespace layout = store::layout;50namespace text = utils::text;5152using utils::optional;535455namespace {565758/// Finds the results file for the latest run of the given test suite.59///60/// \param test_suite Identifier of the test suite to query.61///62/// \return Path to the located database holding the most recent data for the63/// given test suite.64///65/// \throw store::error If no previous results file can be found.66static fs::path67find_latest(const std::string& test_suite)68{69const fs::path store_dir = layout::query_store_dir();70try {71const text::regex preg = text::regex::compile(72F("^results.%s.[0-9]{8}-[0-9]{6}-[0-9]{6}.db$") % test_suite, 0);7374std::string latest;7576const fs::directory dir(store_dir);77for (fs::directory::const_iterator iter = dir.begin();78iter != dir.end(); ++iter) {79const text::regex_matches matches = preg.match(iter->name);80if (matches) {81if (latest.empty() || iter->name > latest) {82latest = iter->name;83}84} else {85// Not a database file; skip.86}87}8889if (latest.empty())90throw store::error(91F("No previous results file found for test suite %s")92% test_suite);9394return store_dir / latest;95} catch (const fs::system_error& e) {96LW(F("Failed to open store dir %s: %s") % store_dir % e.what());97throw store::error(F("No previous results file found for test suite %s")98% test_suite);99} catch (const text::regex_error& e) {100throw store::error(e.what());101}102}103104105/// Computes the identifier of a new tests results file.106///107/// \param test_suite Identifier of the test suite.108/// \param when Timestamp to attach to the identifier.109///110/// \return Identifier of the file to be created.111static std::string112new_id(const std::string& test_suite, const datetime::timestamp& when)113{114const std::string when_datetime = when.strftime("%Y%m%d-%H%M%S");115const int when_ms = static_cast<int>(when.to_microseconds() % 1000000);116return F("%s.%s-%06s") % test_suite % when_datetime % when_ms;117}118119120} // anonymous namespace121122123/// Value to request the creation of a new results file with an automatic name.124///125/// Can be passed to new_db().126const char* layout::results_auto_create_name = "NEW";127128129/// Value to request the opening of the latest results file.130///131/// Can be passed to find_results().132const char* layout::results_auto_open_name = "LATEST";133134135/// Resolves the results file for the given identifier.136///137/// \param id Identifier of the test suite to open.138///139/// \return Path to the requested file, if any.140///141/// \throw store::error If there is no matching entry.142fs::path143layout::find_results(const std::string& id)144{145LI(F("Searching for a results file with id %s") % id);146147if (id == results_auto_open_name) {148const std::string test_suite = test_suite_for_path(fs::current_path());149return find_latest(test_suite);150} else {151const fs::path id_as_path(id);152153if (fs::exists(id_as_path) && !fs::is_directory(id_as_path)) {154if (id_as_path.is_absolute())155return id_as_path;156else157return id_as_path.to_absolute();158} else if (id.find('/') == std::string::npos) {159const fs::path candidate =160query_store_dir() / (F("results.%s.db") % id);161if (fs::exists(candidate)) {162return candidate;163} else {164return find_latest(id);165}166} else {167INV(id.find('/') != std::string::npos);168return find_latest(test_suite_for_path(id_as_path));169}170}171}172173174/// Computes the path to a new database for the given test suite.175///176/// \param id Identifier of the test suite to create.177/// \param root Path to the root of the test suite being run, needed to properly178/// autogenerate the identifiers.179///180/// \return Identifier of the created results file, if applicable, and the path181/// to such file.182layout::results_id_file_pair183layout::new_db(const std::string& id, const fs::path& root)184{185std::string generated_id;186optional< fs::path > path;187188if (id == results_auto_create_name) {189generated_id = new_id(test_suite_for_path(root),190datetime::timestamp::now());191path = query_store_dir() / (F("results.%s.db") % generated_id);192fs::mkdir_p(path.get().branch_path(), 0755);193} else {194path = fs::path(id);195}196197return std::make_pair(generated_id, path.get());198}199200201/// Computes the path to a new database for the given test suite.202///203/// \param root Path to the root of the test suite being run; needed to properly204/// autogenerate the identifiers.205/// \param when Timestamp for the test suite being run; needed to properly206/// autogenerate the identifiers.207///208/// \return Identifier of the created results file, if applicable, and the path209/// to such file.210fs::path211layout::new_db_for_migration(const fs::path& root,212const datetime::timestamp& when)213{214const std::string generated_id = new_id(test_suite_for_path(root), when);215const fs::path path = query_store_dir() / (216F("results.%s.db") % generated_id);217fs::mkdir_p(path.branch_path(), 0755);218return path;219}220221222/// Gets the path to the store directory.223///224/// Note that this function does not create the determined directory. It is the225/// responsibility of the caller to do so.226///227/// \return Path to the directory holding all the database files.228fs::path229layout::query_store_dir(void)230{231const optional< fs::path > home = utils::get_home();232if (home) {233const fs::path& home_path = home.get();234if (home_path.is_absolute())235return home_path / ".kyua/store";236else237return home_path.to_absolute() / ".kyua/store";238} else {239LW("HOME not defined; creating store database in current "240"directory");241return fs::current_path();242}243}244245246/// Returns the test suite name for the current directory.247///248/// \return The identifier of the current test suite.249std::string250layout::test_suite_for_path(const fs::path& path)251{252std::string test_suite;253if (path.is_absolute())254test_suite = path.str();255else256test_suite = path.to_absolute().str();257PRE(!test_suite.empty() && test_suite[0] == '/');258259std::replace(test_suite.begin(), test_suite.end(), '/', '_');260test_suite.erase(0, 1);261262return test_suite;263}264265266