diff --git a/sample/compile.cc b/sample/compile.cc index d6fb141a..d69543ea 100644 --- a/sample/compile.cc +++ b/sample/compile.cc @@ -12,42 +12,37 @@ // See the License for the specific language governing permissions and // limitations under the License. -#include #include +#include #include #include #include #include #include +#include #include - // help -static constexpr char helpstring[] = - "\n Usage: compile infile [outfile]\n" - " infile can be in mjcf, urdf, mjb format\n" - " outfile can be in mjcf, mjb, txt format\n\n" - " if infile is mjcf or urdf and outfile is omitted, a detailed\n" - " timing breakdown is printed for two compilations (cold and warm cache)\n\n" - " Example: compile model.xml [model.mjb]\n"; - - -// timer (seconds) -double gettm(void) { - using Clock = std::chrono::steady_clock; - using Seconds = std::chrono::duration; - static const Clock::time_point tm_start = Clock::now(); - return Seconds(Clock::now() - tm_start).count(); -} - +static constexpr char kHelp[] = + "\n Usage: compile infile [outfile]\n" + " infile can be in any format with a registered decoder (e.g. mjcf, " + "urdf) or mjb\n" + " outfile can be in mjcf, mjb, txt format, or empty\n\n" + " if outfile is empty, compilation will be " + "timed twice to measure the impact of caching\n\n" + " Example: compile model.xml [model.mjb]\n"; // deallocate and print message -int finish(const char* msg = 0, int exitcode = EXIT_SUCCESS, mjModel* m = 0) { +int finish(const char* msg = 0, int exitcode = EXIT_SUCCESS, mjModel* m = 0, + mjVFS* vfs = 0) { // deallocated everything if (m) { mj_deleteModel(m); } + if (vfs) { + mj_deleteVFS(vfs); + } // print message if (msg) { @@ -57,52 +52,20 @@ int finish(const char* msg = 0, int exitcode = EXIT_SUCCESS, mjModel* m = 0) { return exitcode; } +// check if filename has extension (case-insensitive) +bool HasExtension(std::string_view filename, std::string_view ext) { + if (filename.length() < ext.length()) return false; -// possible file types -enum { - typeUNKNOWN = 0, - typeXML, - typeMJB, - typeTXT, - typeNONE -}; - - -// determine file type -int filetype(const char* filename) { - // convert to lower case for string comparison - char lower[1000]; - std::size_t i=0; - while (i=0 && lower[dot]!='.') { - dot--; - } - - // no dot found - if (dot<0) { - return typeUNKNOWN; - } - - // check extension - if (!std::strcmp(lower+dot, ".xml") || !std::strcmp(lower+dot, ".urdf")) { - return typeXML; - } else if (!std::strcmp(lower+dot, ".mjb")) { - return typeMJB; - } else if (!std::strcmp(lower+dot, ".txt")) { - return typeTXT; - } else { - return typeUNKNOWN; + std::string_view file_ext = filename.substr(filename.length() - ext.length()); + for (std::size_t i = 0; i < ext.length(); ++i) { + if (std::tolower(static_cast(file_ext[i])) != + std::tolower(static_cast(ext[i]))) { + return false; + } } + return true; } - // main function int main(int argc, char** argv) { @@ -111,34 +74,33 @@ int main(int argc, char** argv) { char error[1000]; // print help if arguments are missing - if (argc!=3 && argc!=2) { - return finish(helpstring, EXIT_FAILURE); + if (argc != 3 && argc != 2) { + return finish(kHelp, EXIT_FAILURE); } - // determine file types - int type1 = filetype(argv[1]); - int type2 = argc==2 ? typeNONE : filetype(argv[2]); + const bool is_mjb = HasExtension(argv[1], ".mjb"); - // check types - if (type1 == typeUNKNOWN || type1 == typeTXT || - type2 == typeUNKNOWN || (type1 == typeMJB && type2 == typeXML)) { - return finish("Illegal combination of file formats", EXIT_FAILURE); + if (is_mjb && argc == 3 && HasExtension(argv[2], ".xml")) { + return finish("Illegal combination: cannot save binary model to XML", + EXIT_FAILURE); } // check if output file exists - std::FILE* fp = std::fopen(argv[2], "r"); - if (fp) { - std::cout << "Output file already exists, overwrite? (Y/n) "; - char c; - std::cin >> c; - if (c!='y' && c!='Y') { + if (argc == 3) { + std::FILE* fp = std::fopen(argv[2], "r"); + if (fp) { std::fclose(fp); - return finish(); + std::cout << "Output file already exists, overwrite? (Y/n) "; + char c; + std::cin >> c; + if (c != 'y' && c != 'Y') { + return finish(); + } } } // enable compile timing diagnostics - if (type2 == typeNONE) { + if (argc == 2) { mjLogConfig config = mju_getLogConfig(); config.logfile[0] = '\0'; config.topics |= (1 << (mjTOPIC_TIME_CMP - 1)); @@ -146,50 +108,58 @@ int main(int argc, char** argv) { } // load model - mjSpec* s = nullptr; - if (type1==typeXML) { - s = mj_parseXML(argv[1], 0, error, 1000); - if (!s) { - return finish(error, EXIT_FAILURE); - } + mjVFS vfs; + mj_defaultVFS(&vfs); + mjSpec* spec = nullptr; - if (type2 == typeNONE) { - std::cout << "Compile 1 (cold cache)\n"; - } - m = mj_compile(s, 0); + if (is_mjb) { + m = mj_loadModel(argv[1], &vfs); if (!m) { - mj_deleteSpec(s); - return finish("Could not compile model", EXIT_FAILURE); - } - - if (type2 == typeNONE) { - mj_deleteModel(m); - std::cout << "Compile 2 (warm cache)\n"; - m = mj_compile(s, 0); + return finish("Could not load binary model", EXIT_FAILURE, nullptr, &vfs); } } else { - m = mj_loadModel(argv[1], 0); + spec = mj_parse(argv[1], nullptr, &vfs, error, 1000); + if (!spec) { + return finish(error, EXIT_FAILURE, nullptr, &vfs); + } + + if (argc == 2) { + std::cout << "Compile 1 (cold cache)\n"; + } + m = mj_compile(spec, &vfs); + if (!m) { + auto err_msg = mjs_getError(spec); + mj_deleteSpec(spec); + return finish(err_msg, EXIT_FAILURE, nullptr, &vfs); + } + + if (argc == 2) { + mj_deleteModel(m); + std::cout << "Compile 2 (warm cache)\n"; + m = mj_compile(spec, &vfs); + } } // check error if (!m) { - if (s) mj_deleteSpec(s); - return finish("Could not load model", EXIT_FAILURE); + if (spec) { + auto err_msg = mjs_getError(spec); + mj_deleteSpec(spec); + return finish(err_msg, EXIT_FAILURE, nullptr, &vfs); + } else { + return finish("Could not load model", EXIT_FAILURE, nullptr, &vfs); + } } - // save model - if (type2 == typeXML) { - if (!mj_saveLastXML(argv[2], m, error, 1000)) { - if (s) mj_deleteSpec(s); - return finish(error, EXIT_FAILURE, m); + // encode output + if (argc == 3) { + if (mj_encode(spec, m, argv[2], nullptr, &vfs, error, 1000) < 0) { + if (spec) mj_deleteSpec(spec); + return finish(error, EXIT_FAILURE, m, &vfs); } - } else if (type2 == typeMJB) { - mj_saveModel(m, argv[2], 0, 0); - } else if (type2 == typeTXT) { - mj_printModel(m, argv[2]); } // finalize - if (s) mj_deleteSpec(s); - return finish("Done.", EXIT_SUCCESS, m); + if (spec) mj_deleteSpec(spec); + return finish("Done.", EXIT_SUCCESS, m, &vfs); } diff --git a/test/sample/compile_test.sh b/test/sample/compile_test.sh index d45151f1..1029cb87 100755 --- a/test/sample/compile_test.sh +++ b/test/sample/compile_test.sh @@ -35,4 +35,35 @@ if [ ! -s "$OUTPUT_FILE" ]; then die "Output file empty or missing (${OUTPUT_FILE})." fi +# Test invalid arguments (expect failure) +"$TARGET_BINARY" > /dev/null 2>&1 +if [ $? -eq 0 ]; then + die "Expected failure with no arguments" +fi + +# Test illegal combination (MJB -> XML, expect failure) +"$TARGET_BINARY" "$OUTPUT_FILE" "${TEST_TMPDIR}/failed.xml" > /dev/null 2>&1 +if [ $? -eq 0 ]; then + die "Expected failure for MJB -> XML" +fi + +# Test compile with 1 argument (expect success) +"$TARGET_BINARY" "$MODEL" > /dev/null || die "compile with 1 argument failed" + +# Test XML -> MJZ (expect success) +OUTPUT_MJZ="${TEST_TMPDIR}/compiled.mjz" +"$TARGET_BINARY" "$MODEL" "$OUTPUT_MJZ" || die "compile to MJZ failed" + +if [ ! -s "$OUTPUT_MJZ" ]; then + die "MJZ output file empty or missing (${OUTPUT_MJZ})." +fi + +# Test MJZ -> MJB (expect success) +OUTPUT_MJB_FROM_MJZ="${TEST_TMPDIR}/compiled_from_mjz.mjb" +"$TARGET_BINARY" "$OUTPUT_MJZ" "$OUTPUT_MJB_FROM_MJZ" || die "compile from MJZ to MJB failed" + +if [ ! -s "$OUTPUT_MJB_FROM_MJZ" ]; then + die "MJB output from MJZ empty or missing (${OUTPUT_MJB_FROM_MJZ})." +fi + echo "PASS"