diff --git a/luprex/core/Makefile b/luprex/core/Makefile index 75775cc3..196af6b4 100644 --- a/luprex/core/Makefile +++ b/luprex/core/Makefile @@ -4,6 +4,8 @@ CXX=g++ -std=c++17 -Wall -g -Iinc -Icpp CPP_FILES=\ cpp/invocation.cpp\ cpp/spookyv2.cpp\ + cpp/drivenengine.cpp\ + cpp/driver.cpp\ cpp/util.cpp\ cpp/luastack.cpp\ cpp/traceback.cpp\ diff --git a/luprex/core/cpp/drivenengine.cpp b/luprex/core/cpp/drivenengine.cpp index fad987ec..deebcd66 100644 --- a/luprex/core/cpp/drivenengine.cpp +++ b/luprex/core/cpp/drivenengine.cpp @@ -1 +1,175 @@ -#include "channel.hpp" \ No newline at end of file +#include "drivenengine.hpp" + + +Channel::Channel(DrivenEngine *de, int chid, int port, const std::string &target) { + driven_ = de; + chid_ = chid; + sb_in_.reset(new StreamBuffer); + sb_out_.reset(new StreamBuffer); + port_ = port; + closed_ = false; + target_ = target; + assert(driven_->channels_[chid_] == nullptr); + driven_->channels_[chid_] = this; +} + +Channel::~Channel() { + assert(driven_->channels_[chid_] == this); + driven_->channels_.erase(chid_); + if (driven_->recent_channel_ == this) { + driven_->recent_channel_ = driven_->stdio_channel_.get(); + } +} + +double DrivenEngine::get_clock() { + return clock_; +} + +std::unique_ptr DrivenEngine::new_outgoing_channel(const std::string &target) { + return std::unique_ptr(new Channel(this, next_channel_id_++, 0, target)); +} + +std::unique_ptr DrivenEngine::new_incoming_channel() { + if (accepted_channels_.empty()) { + return nullptr; + } else { + std::unique_ptr result = std::move(accepted_channels_.back()); + accepted_channels_.pop_back(); + return std::move(result); + } +} + +Channel *DrivenEngine::get_stdio_channel() { + return stdio_channel_.get(); +} + +std::unique_ptr DrivenEngine::get_lua_source() { + return std::move(lua_source_); +} + +void DrivenEngine::rescan_lua_source() { + rescan_lua_source_ = true; +} + +void DrivenEngine::stop_driver() { + stop_driver_ = true; +} + +void DrivenEngine::drv_logmode_write(const std::string &filename, int64_t maxsize) { + // NOT IMPLEMENTED YET, but it's okay as a stub. +} + +void DrivenEngine::drv_logmode_replay(const std::string &filename) { + // NOT IMPLEMENTED YET. + assert(false); +} + +void DrivenEngine::drv_logmode_none() { + // NOT IMPLEMENTED YET, but it's okay as a stub. +} + +void DrivenEngine::drv_invoke_engine_init() { + init(); +} + +void DrivenEngine::drv_invoke_engine_update() { + update(); +} + +Channel *DrivenEngine::get_chid(int chid) { + // We cache the most recently used channel. + if (recent_channel_->chid_ != chid) { + auto iter = channels_.find(chid); + assert(iter != channels_.end()); + recent_channel_ = iter->second; + } + return recent_channel_; +} + +void DrivenEngine::drv_list_channels(std::vector &channels) { + channels.clear(); + for (const auto &p : channels_) { + if (!p.second->closed_) { + channels.push_back(p.first); + } + } +} + +const std::string &DrivenEngine::drv_get_target(int chid) { + return get_chid(chid)->target_; +} + +void DrivenEngine::drv_peek_outgoing(int chid, int *nbytes, const char **bytes) { + Channel *ch = get_chid(chid); + *nbytes = ch->sb_out_->fill(); + *bytes = ch->sb_out_->data(); +} + +void DrivenEngine::drv_sent_outgoing(int chid, int nbytes) { + get_chid(chid)->sb_out_->read_bytes(nbytes); +} + +void DrivenEngine::drv_recv_incoming(int chid, int nbytes, char *bytes) { + get_chid(chid)->sb_in_->write_bytes(bytes, nbytes); +} + +void DrivenEngine::drv_notify_close(int chid) { + get_chid(chid)->closed_ = true; +} + +int DrivenEngine::drv_notify_accept(int port) { + int chid = next_channel_id_++; + accepted_channels_.emplace_back(new Channel(this, chid, port, "")); + return chid; +} + +void DrivenEngine::drv_set_clock(double t) { + clock_ = t; +} + +void DrivenEngine::drv_set_lua_source(const util::LuaSource &source) { + lua_source_.reset(new util::LuaSource(source)); + rescan_lua_source_ = false; +} + +bool DrivenEngine::drv_get_rescan_lua_source() { + return rescan_lua_source_; +} + +bool DrivenEngine::drv_get_stop_driver() { + return stop_driver_; +} + +bool DrivenEngine::drv_step_logfile() { + // NOT IMPLEMENTED + assert(false); + return false; +} + +DrivenEngine::DrivenEngine() { + next_channel_id_ = 1; + stdio_channel_.reset(new Channel(this, 0, 0, "")); + recent_channel_ = stdio_channel_.get(); + rescan_lua_source_ = true; + clock_ = 0.0; + stop_driver_ = false; +} + +DrivenEngine::~DrivenEngine() { + // Delete the channels that we own. + stdio_channel_.reset(); + accepted_channels_.clear(); + // At this point, all channels should be gone. + assert(channels_.empty()); +} + +static DrivenEngine *engine_; + +void DrivenEngine::set(DrivenEngine *de) { + engine_ = de; +} + +DrivenEngine *DrivenEngine::get() { + return engine_; +} + diff --git a/luprex/core/cpp/drivenengine.hpp b/luprex/core/cpp/drivenengine.hpp index 3583ad2d..89146f9b 100644 --- a/luprex/core/cpp/drivenengine.hpp +++ b/luprex/core/cpp/drivenengine.hpp @@ -73,6 +73,9 @@ // // * Repeat the following steps over and over: // +// - If the engine asked that the lua source be refreshed, read the source +// from disk and call 'drv_set_lua_source'. +// // - List all existing channels using drv_list_channels. // // - If there are any new channels in the channel list, use @@ -101,8 +104,7 @@ // drv_peek_outgoing, drv_sent_outgoing, and drv_recv_incoming in the // same manner as you would for a socket. // -// - Use 'drv_invoke_engine_update' to invoke the engine's update -// callback. +// - Use 'drv_invoke_engine_update' to invoke the engine's update callback. // ////////////////////////////////////////////////////////////// @@ -111,25 +113,19 @@ #include #include +#include +#include #include "streambuffer.hpp" #include "util.hpp" +class DrivenEngine; + class Channel { -private: - int chid_; - std::unique_ptr sb_in_; - std::unique_ptr sb_out_; - int port_; - bool remote_closed_; - std::string target_; - public: // Get the buffers associated with this channel. // - StreamBuffer *out(); - StreamBuffer *in(); - const StreamBuffer *out() const; - const StreamBuffer *in() const; + StreamBuffer *out() { return sb_out_.get(); } + StreamBuffer *in() { return sb_in_.get(); } // If this is a socket connection, the receiver's port number. // @@ -140,30 +136,32 @@ public: // True if the remote closed the connection, or a failure occurred. // - bool remote_closed() const { return remote_closed_; } + bool closed() const { return closed_; } - // If communications were closed by the remote, there may - // be an error message. + // You may delete any channel except for stdio. This closes + // the channel. // - std::string errmsg() const { return errmsg_; } + ~Channel(); + +private: + // Constructor is deliberately private. Use + // DrivenEngine::new_outgoing_channel to create outgoing socket channels. + // + Channel(DrivenEngine *de, int chid, int port, const std::string &target); + +private: + DrivenEngine *driven_; + int chid_; + std::unique_ptr sb_in_; + std::unique_ptr sb_out_; + int port_; + bool closed_; + std::string target_; + friend class DrivenEngine; }; class DrivenEngine { public: - // Constructor. - // - // Most initialization is achieved by 'drv_xxx' functions, so - // this constructor takes no arguments. - // - DrivenEngine(); - - // Destructor. - // - // It is necessary to delete all channels before deleting the - // DrivenEngine. The destructor will verify that this has been done. - // - ~DrivenEngine(); - ////////////////////////////////////////////////////////////// // // The following methods are the 'engine' side of the pipe. @@ -174,7 +172,7 @@ public: // This will be called to initialize the logic engine, shortly after the lua // source is loaded. // - virtual void init() { } + virtual void init() {} // The update function. You should override this in a subclass. This will // be called to give the engine a chance to respond to new data. @@ -195,7 +193,7 @@ public: // actually opening the connection and relaying data into the channel using // drv_get_target, drv_peek_outgoing, drv_sent_outgoing, drv_recv_incoming. // - std::unique_ptr new_outgoing_channel(const std::string &target) + std::unique_ptr new_outgoing_channel(const std::string &target); // Create a new channel from any pending incoming connection. If there is no // incoming connection, returns nullptr. @@ -234,7 +232,7 @@ public: // periodically poll to see if the engine has called rescan_lua_source, // using drv_get_rescan // - std::unique_ptr get_lua_source(); + std::unique_ptr get_lua_source(); // Rescan the lua source directory. The lua source directory is read once, // automatically, at engine creation time. If you want to read it again, @@ -245,6 +243,11 @@ public: // void rescan_lua_source(); + // Stop the driver. The engine should call this when it's done + // and there's nothing left to do. + // + void stop_driver(); + ////////////////////////////////////////////////////////////// // // The following methods are the 'driver' side of the pipe. @@ -293,7 +296,7 @@ public: // is supposed to be talking to. Non-socket channels and incoming channels // have empty targets. // - std::string drv_get_target(int chid); + const std::string &drv_get_target(int chid); // Get a pointer to the bytes in the outgoing buffer. The pointer returned // here is naturally only valid until the buffer is changed. This function @@ -338,7 +341,7 @@ public: // Set the lua source code. The driver is expected to read the lua source // code and store it (using this function) once before invoking // - void drv_set_lua_source(const util::StringMap &source); + void drv_set_lua_source(const util::LuaSource &source); // Check the 'rescan_lua_source' flag. If this flag is set, it means // that the engine wants the driver to rescan the lua source code. @@ -347,19 +350,59 @@ public: // bool drv_get_rescan_lua_source(); + // If true, the engine is done. Stop the driver. + // + bool drv_get_stop_driver(); + // In replay mode, perform a single step of the logfile. Returns true // if the logfile was not empty. // bool drv_step_logfile(); + ////////////////////////////////////////////////////////////// + // + // Creation and Destruction. + // + ////////////////////////////////////////////////////////////// + + // Constructor. + // + // Most initialization is achieved by 'drv_xxx' functions, so + // this constructor takes no arguments. + // + DrivenEngine(); + + // Destructor. + // + // It is necessary to delete all channels before deleting the + // DrivenEngine. The destructor will verify that this has been done. + // + virtual ~DrivenEngine(); + + // Set/Get Global Pointer. + // + // Normally, there is a single global "DrivenEngine" instance. + // We provide a global pointer to store this instance. This is + // a raw pointer, you must manually delete the DrivenEngine. + // + static void set(DrivenEngine *de); + static DrivenEngine *get(); + private: - Channel *stdio_channel_; + // Get a channel by channel ID. + Channel *get_chid(int chid); + +private: + std::unique_ptr stdio_channel_; int next_channel_id_; std::map channels_; Channel *recent_channel_; - std::vector accepted_channels_; + std::vector> accepted_channels_; bool rescan_lua_source_; - std::unique_ptr lua_source_; + std::unique_ptr lua_source_; + double clock_; + bool stop_driver_; + friend class Channel; }; #endif // DRIVENENGINE_HPP diff --git a/luprex/core/cpp/driver.cpp b/luprex/core/cpp/driver.cpp new file mode 100644 index 00000000..1e7e46ad --- /dev/null +++ b/luprex/core/cpp/driver.cpp @@ -0,0 +1,32 @@ + +#include "driver.hpp" +#include +#include +#include +#include + + +void driver_drive(DrivenEngine *de) { + const int MAXINPUT = 1000; + char buf[MAXINPUT]; + int nbytes; const char *bytes; + DrivenEngine::set(de); + de->drv_logmode_none(); + de->drv_set_lua_source(util::read_lua_source("lua")); + de->drv_invoke_engine_init(); + while (!de->drv_get_stop_driver()) { + if (de->drv_get_rescan_lua_source()) { + de->drv_set_lua_source(util::read_lua_source("lua")); + } + de->drv_peek_outgoing(0, &nbytes, &bytes); + if (nbytes > 0) { + fwrite(bytes, 1, nbytes, stdout); + } + if (fgets(buf, MAXINPUT, stdin)) { + de->drv_recv_incoming(0, strlen(buf), buf); + } + de->drv_invoke_engine_update(); + } + DrivenEngine::set(nullptr); +} + diff --git a/luprex/core/cpp/driver.hpp b/luprex/core/cpp/driver.hpp new file mode 100644 index 00000000..ed24bf48 --- /dev/null +++ b/luprex/core/cpp/driver.hpp @@ -0,0 +1,8 @@ +#ifndef DRIVER_HPP +#define DRIVER_HPP + +#include "drivenengine.hpp" + +void driver_drive(DrivenEngine *de); + +#endif // DRIVER_HPP diff --git a/luprex/core/cpp/main.cpp b/luprex/core/cpp/main.cpp index 0ecdd19c..2ef99c9a 100644 --- a/luprex/core/cpp/main.cpp +++ b/luprex/core/cpp/main.cpp @@ -1,8 +1,9 @@ #include "textgame.hpp" +#include "driver.hpp" int main(int argc, char **argv) { TextGame tg; - tg.run(); + driver_drive(&tg); } diff --git a/luprex/core/cpp/source.cpp b/luprex/core/cpp/source.cpp index 3aae50a8..d9e6e0a7 100644 --- a/luprex/core/cpp/source.cpp +++ b/luprex/core/cpp/source.cpp @@ -15,24 +15,6 @@ #include "source.hpp" #include "luasnap.hpp" -// Read control.lst -// -// - trim all lines -// - remove blank lines -// - remove comment lines -// -util::StringVec read_control_lst(const std::string &path) { - util::StringVec lines = util::get_file_lines(path); - util::StringVec result; - for (int i = 0; i < int(lines.size()); i++) { - std::string trimmed = util::trim(lines[i]); - if ((trimmed.size() > 0) && (trimmed[0] != '#')) { - result.push_back(trimmed); - } - } - return result; -} - LuaDefine(source_makeclass, "f") { LuaArg classname; LuaRet classtab; @@ -155,17 +137,7 @@ static void source_updatefile(LuaStack &LS0, LuaSlot source, LuaSlot fn, LuaSlot old_fingerprint = LS.ckstring(fingerprint); } // std::cerr << "Probing " << cfn << std::endl; - std::string new_fingerprint = util::get_file_fingerprint("lua/" + cfn); - if ((old_fingerprint == "") || (old_fingerprint != new_fingerprint)) { - std::cerr << "Rereading " << cfn << std::endl; - std::string code; - if (new_fingerprint != "") code = util::get_file_contents("lua/" + cfn); - LS.rawset(info, "name", fn); - LS.rawset(info, "fingerprint", new_fingerprint); - LS.rawset(info, "code", code); - LS.rawset(info, "hash", util::hash_to_hex(util::hash_string(code))); - calculate_loadresult(LS, info, cfn, code); - } + LS.result(); } @@ -295,39 +267,31 @@ std::string SourceDB::get(const std::string &fn) { return oss.str(); } -void SourceDB::update() { +void SourceDB::update(const util::LuaSource &source) { lua_State *L = lua_state_; - LuaVar sourcedb, newdb, info, fn, seq; - LuaStack LS(L, newdb, sourcedb, info, fn, seq); + LuaVar sourcedb, info; + LuaStack LS(L, sourcedb, info); - // Get the (old) source database. + // Get and clear the source database. LS.rawget(sourcedb, LuaRegistry, "sourcedb"); if (!LS.istable(sourcedb)) { LS.newtable(sourcedb); + LS.rawset(LuaRegistry, "sourcedb", sourcedb); } + LS.cleartable(sourcedb); - // Read the list of filenames. - util::StringVec filenames = read_control_lst("lua/control.lst"); - if (filenames.empty()) { - luaL_error(L, "cannot read source database control.lst"); + for (int i = 0; i < int(source.size()); i++) { + const std::string &file = source[i].first; + const std::string &code = source[i].second; + std::cerr << "Compiling " << file << std::endl; + LS.newtable(info); + LS.rawset(info, "name", file); + LS.rawset(info, "code", code); + LS.rawset(info, "hash", util::hash_to_hex(util::hash_string(code))); + LS.rawset(info, "sequence", i + 1); + calculate_loadresult(LS, info, file, code); + LS.rawset(sourcedb, file, info); } - - // Process the files one by one. - LS.newtable(newdb); - for (int i = 0; i < int(filenames.size()); i++) { - LS.set(fn, filenames[i]); - - // Call source_updatefile to get the updated info for one file. - source_updatefile(LS, sourcedb, fn, info); - - // Insert the sequence number and put finalized info into the new database. - LS.set(seq, i + 1); - LS.rawset(info, "sequence", seq); - LS.rawset(newdb, fn, info); - } - - // Store the new source db. - LS.rawset(LuaRegistry, "sourcedb", newdb); LS.result(); } diff --git a/luprex/core/cpp/source.hpp b/luprex/core/cpp/source.hpp index 35558546..d6bbb9f5 100644 --- a/luprex/core/cpp/source.hpp +++ b/luprex/core/cpp/source.hpp @@ -132,12 +132,10 @@ public: // Update // - // Read all the lua source files from disk and store them in the - // source database. Also compiles these files using lua's "load" - // function. Efficient: if a source file is already in the database - // and hasn't been modified, it is not reloaded. + // Update the database using the specified lua source code. + // Compiles these files using lua's "load" function. // - void update(); + void update(const util::LuaSource &source); // Rebuild // diff --git a/luprex/core/cpp/streambuffer.cpp b/luprex/core/cpp/streambuffer.cpp index 2dd7f067..86538a12 100644 --- a/luprex/core/cpp/streambuffer.cpp +++ b/luprex/core/cpp/streambuffer.cpp @@ -111,6 +111,19 @@ void StreamBuffer::clear() { lua_reader_size_ = 0; } +std::string StreamBuffer::readline() { + char *p = read_cursor_; + while ((p < write_cursor_) && (*p != '\n')) p++; + if (p == write_cursor_) { + return ""; + } else { + p++; + std::string result(read_cursor_, p - read_cursor_); + read_cursor_ = p; + return result; + } +} + // These routines return true if you can losslessly cast the // specified value to the specified type. diff --git a/luprex/core/cpp/streambuffer.hpp b/luprex/core/cpp/streambuffer.hpp index a34512e7..a1163dd4 100644 --- a/luprex/core/cpp/streambuffer.hpp +++ b/luprex/core/cpp/streambuffer.hpp @@ -266,6 +266,11 @@ public: // Frees up as much space as possible. void clear(); + // Attempt to do a "readline". If there is no newline in + // the buffer, returns empty string. If there is a newline, + // returns a block of text that ends in newline. + std::string readline(); + // Write block of bytes into the buffer. // // Caution: this function doesn't write the length! diff --git a/luprex/core/cpp/textgame.cpp b/luprex/core/cpp/textgame.cpp index dd233022..bd9c0d32 100644 --- a/luprex/core/cpp/textgame.cpp +++ b/luprex/core/cpp/textgame.cpp @@ -144,6 +144,7 @@ void TextGame::do_quit_command(const StringVec &cmd) { return; } actor_id_ = 0; + stop_driver(); } void TextGame::do_command(const StringVec &words) { @@ -169,18 +170,28 @@ void TextGame::check_redirects() { } } -void TextGame::run() +void TextGame::init() { world_.reset(new World(util::WORLD_TYPE_STANDALONE)); - world_->update_source(); + std::unique_ptr lsource = get_lua_source(); + world_->update_source(*lsource); world_->run_unittests(); actor_id_ = world_->create_login_actor(); std::cerr << "Login actor ID: " << actor_id_ << std::endl; console_.clear(); +} + +void TextGame::update() { + check_redirects(); + if (actor_id_ == 0) { + stop_driver(); + return; + } + // Process lines from stdin. while (true) { - check_redirects(); - if (actor_id_ == 0) break; - console_.add_stdin(); + std::string line = get_stdio_channel()->in()->readline(); + if (line == "") break; + console_.add(line); int action = console_.action(); if (action == LuaConsole::DO_LUA) { do_lua(console_.lua_expression()); @@ -190,5 +201,8 @@ void TextGame::run() std::cerr << console_.syntax() << std::endl; } } + // Process lua source if available. + std::unique_ptr source = get_lua_source(); + if (source != nullptr) world_->update_source(*source); } diff --git a/luprex/core/cpp/textgame.hpp b/luprex/core/cpp/textgame.hpp index 7cd2baa3..3b4ff258 100644 --- a/luprex/core/cpp/textgame.hpp +++ b/luprex/core/cpp/textgame.hpp @@ -4,9 +4,10 @@ #include "luaconsole.hpp" #include "world.hpp" +#include "drivenengine.hpp" #include -class TextGame { +class TextGame : public DrivenEngine { private: using StringVec = LuaConsole::StringVec; std::unique_ptr world_; @@ -27,7 +28,8 @@ private: void check_redirects(); public: - void run(); + virtual void init(); + virtual void update(); }; #endif // TEXTGAME_HPP diff --git a/luprex/core/cpp/util.cpp b/luprex/core/cpp/util.cpp index 6ea043cc..4e6a35bd 100644 --- a/luprex/core/cpp/util.cpp +++ b/luprex/core/cpp/util.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #ifndef WIN32 #include @@ -206,36 +207,34 @@ double distance_squared(double x1, double y1, double x2, double y2) { return dx*dx + dy*dy; } -std::string get_file_contents(const std::string &fn) { +static std::string get_file_contents(const std::string &fn) { std::ifstream fs(fn); std::stringstream buffer; buffer << fs.rdbuf(); return buffer.str(); } -StringVec get_file_lines(const std::string &path) { - StringVec result; - std::ifstream f; - f.open(path); - if (f) { - std::string line; - while (std::getline(f, line)) { - result.push_back(line); +static StringVec read_control_lst(const std::string &path) { + StringVec lines = split(get_file_contents(path), '\n'); + util::StringVec result; + for (int i = 0; i < int(lines.size()); i++) { + std::string trimmed = trim(lines[i]); + if ((trimmed.size() > 0) && (trimmed[0] != '#')) { + result.push_back(trimmed); } - f.close(); } return result; } -std::string get_file_fingerprint(const std::string &fn) { - struct stat result; - if(stat(fn.c_str(), &result)==0) - { - std::stringstream ss; - ss << result.st_mtime; - return ss.str(); +LuaSource read_lua_source(const std::string &dir) { + StringVec files = read_control_lst(dir + "/control.lst"); + assert (!files.empty()); + LuaSource result; + for (const std::string &file : files) { + std::string data = get_file_contents(dir + "/" + file); + result.emplace_back(file, data); } - return ""; + return result; } std::string XYZ::debug_string() const { diff --git a/luprex/core/cpp/util.hpp b/luprex/core/cpp/util.hpp index b90fb498..4a69f3e1 100644 --- a/luprex/core/cpp/util.hpp +++ b/luprex/core/cpp/util.hpp @@ -3,6 +3,7 @@ #include #include +#include #include #include #include @@ -21,7 +22,8 @@ enum WorldType { }; using StringVec = std::vector; -using StringMap = std::map; +using StringPair = std::pair; +using LuaSource = std::vector; using HashValue = std::pair; using IdVector = std::vector; @@ -73,14 +75,8 @@ std::string trim(std::string s); // Calculate distance between two points double distance_squared(double x1, double y1, double x2, double y2); -// Read a file as one big string. -std::string get_file_contents(const std::string &path); - -// Read a file as a vector of lines. -StringVec get_file_lines(const std::string &path); - -// Get a file's fingerprint - ie, size and modification time. -std::string get_file_fingerprint(const std::string &path); +// Read the lua source code from the specified directory. +LuaSource read_lua_source(const std::string &directory); // An XYZ coordinate, general purpose. struct XYZ { diff --git a/luprex/core/cpp/world.hpp b/luprex/core/cpp/world.hpp index ffd8753f..03a1bd7c 100644 --- a/luprex/core/cpp/world.hpp +++ b/luprex/core/cpp/world.hpp @@ -174,7 +174,7 @@ public: // Update the source database from disk. // - void update_source() { source_db_.update(); source_db_.rebuild(true); } + void update_source(const util::LuaSource &source) { source_db_.update(source); source_db_.rebuild(true); } // Run all unit tests. //