diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..837fdb8 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,1489 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "addr2line" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "aho-corasick" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" +dependencies = [ + "memchr", +] + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "backtrace" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "base64" +version = "0.21.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" + +[[package]] +name = "bumpalo" +version = "3.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" + +[[package]] +name = "cc" +version = "1.0.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +dependencies = [ + "libc", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-targets", +] + +[[package]] +name = "colored" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2674ec482fbc38012cf31e6c42ba0177b431a0cb6f15fe40efa5aab1bda516f6" +dependencies = [ + "is-terminal", + "lazy_static", + "windows-sys", +] + +[[package]] +name = "core-foundation" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" + +[[package]] +name = "difflib" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" + +[[package]] +name = "downcast" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1435fa1053d8b2fbbe9be7e97eca7f33d37b28409959813daefc1446a14247f1" + +[[package]] +name = "either" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" + +[[package]] +name = "encoding_rs" +version = "0.8.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "errno" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f258a7194e7f7c2a7837a8913aeab7fd8c383457034fa20ce4dd3dcb813e8eb8" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "fastrand" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" + +[[package]] +name = "float-cmp" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" +dependencies = [ + "num-traits", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fragile" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c2141d6d6c8512188a7891b4b01590a45f6dac67afb4f255c4124dbb86d4eaa" + +[[package]] +name = "futures-channel" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff4dd66668b557604244583e3e1e1eada8c5c2e96a6d0d6653ede395b78bbacb" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb1d22c66e66d9d72e1758f0bd7d4fd0bee04cad842ee34587d68c07e45d088c" + +[[package]] +name = "futures-sink" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e36d3378ee38c2a36ad710c5d30c2911d752cb941c00c72dbabfb786a7970817" + +[[package]] +name = "futures-task" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efd193069b0ddadc69c46389b740bbccdd97203899b48d09c5f7969591d6bae2" + +[[package]] +name = "futures-util" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a19526d624e703a3179b3d322efec918b6246ea0fa51d41124525f00f1cc8104" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "pin-utils", +] + +[[package]] +name = "gimli" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0" + +[[package]] +name = "h2" +version = "0.3.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d6250322ef6e60f93f9a2162799302cd6f68f79f6e5d85c8c16f14d1d958178" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f93e7192158dbcda357bdec5fb5788eebf8bbac027f3f33e719d29135ae84156" + +[[package]] +name = "hermit-abi" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" + +[[package]] +name = "http" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8947b1a6fad4393052c7ba1f4cd97bed3e953a95c79c92ad9b051a04611d9fbb" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" +dependencies = [ + "bytes", + "http", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "0.14.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffb1cfd654a8219eaef89881fdb3bb3b1cdc5fa75ded05d6933b2b382e395468" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2 0.4.10", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper-tls" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +dependencies = [ + "bytes", + "hyper", + "native-tls", + "tokio", + "tokio-native-tls", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8326b86b6cff230b97d0d312a6c40a60726df3332e721f72a1b035f451663b20" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "idna" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "indexmap" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "ipnet" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" + +[[package]] +name = "is-terminal" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" +dependencies = [ + "hermit-abi", + "rustix", + "windows-sys", +] + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" + +[[package]] +name = "js-sys" +version = "0.3.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54c0c35952f67de54bb584e9fd912b3023117cbafc0a77d8f3dee1fb5f572fe8" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" + +[[package]] +name = "libusb1-sys" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d0e2afce4245f2c9a418511e5af8718bcaf2fa408aefb259504d1a9cb25f27" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "969488b55f8ac402214f3f5fd243ebb7206cf82de60d3172994707a4bcc2b829" + +[[package]] +name = "log" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" + +[[package]] +name = "memchr" +version = "2.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "miniz_oxide" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +dependencies = [ + "adler", +] + +[[package]] +name = "mio" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dce281c5e46beae905d4de1870d8b1509a9142b62eedf18b443b011ca8343d0" +dependencies = [ + "libc", + "wasi", + "windows-sys", +] + +[[package]] +name = "mockall" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c84490118f2ee2d74570d114f3d0493cbf02790df303d2707606c3e14e07c96" +dependencies = [ + "cfg-if", + "downcast", + "fragile", + "lazy_static", + "mockall_derive", + "predicates", + "predicates-tree", +] + +[[package]] +name = "mockall_derive" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22ce75669015c4f47b289fd4d4f56e894e4c96003ffdf3ac51313126f94c6cbb" +dependencies = [ + "cfg-if", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "native-tls" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" +dependencies = [ + "lazy_static", + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "normalize-line-endings" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" + +[[package]] +name = "num-traits" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" +dependencies = [ + "autocfg", +] + +[[package]] +name = "object" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" + +[[package]] +name = "openssl" +version = "0.10.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79a4c6c3a2b158f7f8f2a2fc5a969fa3a068df6fc9dbb4a43845436e3af7c800" +dependencies = [ + "bitflags 2.4.1", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.39", +] + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-sys" +version = "0.9.96" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3812c071ba60da8b5677cc12bcb1d42989a65553772897a7e0355545a819838f" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "phf" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" +dependencies = [ + "phf_macros", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0" +dependencies = [ + "phf_shared", + "rand", +] + +[[package]] +name = "phf_macros" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3444646e286606587e49f3bcf1679b8cef1dc2c5ecc29ddacaffc305180d464b" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn 2.0.39", +] + +[[package]] +name = "phf_shared" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" + +[[package]] +name = "predicates" +version = "2.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59230a63c37f3e18569bdb90e4a89cbf5bf8b06fea0b84e65ea10cc4df47addd" +dependencies = [ + "difflib", + "float-cmp", + "itertools", + "normalize-line-endings", + "predicates-core", + "regex", +] + +[[package]] +name = "predicates-core" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b794032607612e7abeb4db69adb4e33590fa6cf1149e95fd7cb00e634b92f174" + +[[package]] +name = "predicates-tree" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368ba315fb8c5052ab692e68a0eefec6ec57b23a36959c14496f0b0df2c0cecf" +dependencies = [ + "predicates-core", + "termtree", +] + +[[package]] +name = "proc-macro2" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" + +[[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "regex" +version = "1.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" + +[[package]] +name = "reqwest" +version = "0.11.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "046cd98826c46c2ac8ddecae268eb5c2e58628688a5fc7a2643704a73faba95b" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "hyper", + "hyper-tls", + "ipnet", + "js-sys", + "log", + "mime", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "serde_urlencoded", + "system-configuration", + "tokio", + "tokio-native-tls", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "winreg", +] + +[[package]] +name = "rpassword" +version = "7.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80472be3c897911d0137b2d2b9055faf6eeac5b14e324073d83bc17b191d7e3f" +dependencies = [ + "libc", + "rtoolbox", + "windows-sys", +] + +[[package]] +name = "rtoolbox" +version = "0.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c247d24e63230cdb56463ae328478bd5eac8b8faa8c69461a77e8e323afac90e" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "rusb" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45fff149b6033f25e825cbb7b2c625a11ee8e6dac09264d49beb125e39aa97bf" +dependencies = [ + "libc", + "libusb1-sys", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" + +[[package]] +name = "rustix" +version = "0.38.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc99bc2d4f1fed22595588a013687477aedf3cdcfb26558c559edb67b4d9b22e" +dependencies = [ + "bitflags 2.4.1", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "ryu" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" + +[[package]] +name = "schannel" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c3733bf4cf7ea0880754e19cb5a462007c4a8c1914bff372ccc95b464f1df88" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "security-framework" +version = "2.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "serde" +version = "1.0.193" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25dd9975e68d0cb5aa1120c288333fc98731bd1dd12f561e468ea4728c042b89" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.193" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.39", +] + +[[package]] +name = "serde_json" +version = "1.0.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "socket2" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7916fc008ca5542385b89a3d3ce689953c143e9304a9bf8beec1de48994c0d" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "socket2" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23e78b90f2fcf45d3e842032ce32e3f2d1545ba6636271dcbf24fa306d87be7a" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "system-configuration" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "tempfile" +version = "3.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ef1adac450ad7f4b3c28589471ade84f25f731a7a0fe30d71dfa9f60fd808e5" +dependencies = [ + "cfg-if", + "fastrand", + "redox_syscall", + "rustix", + "windows-sys", +] + +[[package]] +name = "termtree" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76" + +[[package]] +name = "thiserror" +version = "1.0.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9a7210f5c9a7156bb50aa36aed4c95afb51df0df00713949448cf9e97d382d2" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.39", +] + +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tmc" +version = "0.1.1" +source = "git+https://github.com/esarver/rusb-usbtmc#0ed58c406a9ec7f1c48672a74531d8cd70957ac7" +dependencies = [ + "byteorder", + "rusb", + "thiserror", +] + +[[package]] +name = "tokio" +version = "1.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0c014766411e834f7af5b8f4cf46257aab4036ca95e9d2c144a10f59ad6f5b9" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2 0.5.5", + "windows-sys", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", + "tracing", +] + +[[package]] +name = "tower-service" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" + +[[package]] +name = "tracing" +version = "0.1.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.39", +] + +[[package]] +name = "tracing-core" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" + +[[package]] +name = "tsp-tookit-kic-lib" +version = "0.10.2" +dependencies = [ + "anyhow", + "bytes", + "chrono", + "colored", + "mockall", + "phf", + "reqwest", + "rpassword", + "rusb", + "serde", + "serde_json", + "thiserror", + "tmc", + "tracing", +] + +[[package]] +name = "unicode-bidi" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "unicode-normalization" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "url" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.88" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7daec296f25a1bae309c0cd5c29c4b260e510e6d813c286b19eaadf409d40fce" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.88" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e397f4664c0e4e428e8313a469aaa58310d302159845980fd23b0f22a847f217" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.39", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afec9963e3d0994cac82455b2b3502b81a7f40f9a0d32181f7528d9f4b43e02" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.88" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5961017b3b08ad5f3fe39f1e79877f8ee7c23c5e5fd5eb80de95abc41f1f16b2" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.88" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5353b8dab669f5e10f5bd76df26a9360c748f054f862ff5f3f8aae0c7fb3907" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.39", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.88" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d046c5d029ba91a1ed14da14dca44b68bf2f124cfbaf741c54151fdb3e0750b" + +[[package]] +name = "web-sys" +version = "0.3.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5db499c5f66323272151db0e666cd34f78617522fb0c1604d31a27c50c206a85" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-core" +version = "0.51.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1f8cf84f35d2db49a46868f947758c7a1138116f7fac3bc844f43ade1292e64" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "winreg" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +dependencies = [ + "cfg-if", + "windows-sys", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..6678e13 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "tsp-tookit-kic-lib" +description = "A library specifically enabling communication to the Keithley product-line of instruments" +version = "0.10.2" +authors = ["Keithley Instruments, LLC"] +edition = "2021" +repository = "https://git.keithley.com/trebuchet/teaspoon/ki-comms" + +[dependencies] +bytes = "1.4.0" +phf = { version = "0.11.1", features = ["macros"] } +reqwest = "0.11" +rpassword = "7.2.0" +serde = { version = "1.0.152", features = ["derive"] } +serde_json = "1.0.93" +thiserror = "1.0.38" +tmc = { git = "https://github.com/esarver/rusb-usbtmc" } +tracing = { version = "0.1", features = ["async-await"] } +rusb = "0.9.1" +chrono = "0.4.30" +#oklib-rs = { git = "https://git.keithley.com/trebuchet/teaspoon/oklib-rs.git", branch = "main", optional = true } + +[dev-dependencies] +anyhow = "1.0.69" +bytes = "1" +colored = "2.0.0" +mockall = { version = "0.11.4", features = ["nightly"] } diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..b588c69 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,149 @@ +//! All the errors that this crate can emit are defined in the +//! [`error::InstrumentError`] enum. + +use std::{num::ParseIntError, string::FromUtf8Error}; + +use thiserror::Error; + +use crate::instrument::info::ConnectionAddr; + +/// Define errors that originate from this crate +#[derive(Error, Debug)] +#[allow(clippy::module_name_repetitions)] +pub enum InstrumentError { + /// The `unparsable_string` was passed where an address was expected, but + /// it couldn't be parsed to a valid address. + #[error("unable to parse `{unparsable_string}`, expected an address")] + AddressParsingError { + ///The string that couldn't be parsed + unparsable_string: String, + }, + + /// The [`ConnectionAddr`] was not able to be converted to the desired device + /// interface type + #[error("unable to convert from `{from:?}` to {to}")] + ConnectionAddressConversionError { + /// The address information trying to be converted from + from: ConnectionAddr, + /// A string of name of the type trying to be converted to + to: String, + }, + + /// There was an error while trying to connect to the interface or instrument. + #[error("connection error occurred: {details}")] + ConnectionError { + // The details of the [`ConnectionError`] + details: String, + }, + + #[cfg(feature = "debugging")] + /// There was an error when performing a debugging action. + #[error("debug error occurred: {details}")] + DebugError { + /// The details of the [`DebugError`] + details: String, + }, + + #[cfg(feature = "debugging")] + /// The Debugger license was not accepted. + #[error("debug license was not valid: {reason}")] + DebugLicenseRejection { + /// The reason the license was rejected + reason: String, + }, + + /// There was an issue while disconnecting from an instrument. + #[error("unable to gracefully disconnect from instrument: {details}")] + DisconnectError { + /// More information about the disconnection error. + details: String, + }, + + /// A resource file was unable to be decrypted. + #[error("unable to decrypt resource: {source}")] + ResourceDecryptError { + ///**TODO:** Change this to the error that is produced when decryption fails + #[from] + source: FromUtf8Error, + }, + + /// An error that occurs while trying to retrieve information about an instrument + /// such as the serial number, model, manufacturer, etc. + #[error("instrument information retrieval error: {details}")] + InformationRetrievalError { + /// Any extra information about why the instrument information could not be + /// retrieved. + details: String, + }, + + /// An Error that originates from an instrument. This is generic for all instruments + /// and is therefore just a [`String`]. + #[error("{error}")] + InstrumentError { + /// The error string provided by the instrument. + error: String, + }, + + /// Converts a [`std::io::Error`] to a [`TeaspoonInterfaceError`] + #[error("IO error: {source}")] + IoError { + /// The [`std::io::Error`] from which this [`TeaspoonInterfaceError::IoError`] + /// was derived. + #[from] + source: std::io::Error, + }, + + /// The provided login details were either incorrect or the instrument is already + /// claimed and cannot be claimed again. + #[error("provided login details rejected or instrument already claimed")] + LoginRejected, + + #[error("{source}")] + ParseIntError { + #[from] + source: ParseIntError, + }, + + /// An error with communicating through rusb to the instrument + #[error("rusb error: {source}")] + RusbError { + #[from] + source: rusb::Error, + }, + + /// The TSP error that was received from the instrument was malformed. + #[error("unable to parse TSP error from instrument {error}")] + TspErrorParseError { + /// The text of the malformed error that was provided by the instrument + error: String, + }, + + /// Converts a [`tmc::TMCError`] to a [`TeaspoonInterfaceError`] + #[error("USBTMC error: {source}")] + TmcError { + /// The [`tmc::TMCError`] from which this [`TeaspoonInterfaceError::TmcError`] + /// was derived. + #[from] + source: tmc::TMCError, + }, + + /// The queried instrument returned an unknown model number + #[error("\"{model}\" is not a recognized model number")] + UnknownInstrumentModel { + /// The unknown model number + model: String, + }, + + /// The queried instrument returned an unknown language type + #[error("\"{lang} is not a recognized embedded instrument language\"")] + UnknownLanguage { + /// The unknown language type + lang: String, + }, + + /// An uncategorized error. + #[error("{0}")] + Other(String), +} + +pub(crate) type Result = std::result::Result; diff --git a/src/instrument/firmware.rs b/src/instrument/firmware.rs new file mode 100644 index 0000000..51d0a82 --- /dev/null +++ b/src/instrument/firmware.rs @@ -0,0 +1,12 @@ +use crate::error::Result; + +/// The trait an instrument must implement in order to flash the firmware onto an +/// instrument. +pub trait Flash { + /// The method to flash a firmware image to an instrument. + /// + /// # Errors + /// An error can occur in the write to or reading from the instrument as well as in + /// reading the firmware image. + fn flash_firmware(&mut self, image: &[u8], firmware_info: Option) -> Result<()>; +} diff --git a/src/instrument/info.rs b/src/instrument/info.rs new file mode 100644 index 0000000..ff4efba --- /dev/null +++ b/src/instrument/info.rs @@ -0,0 +1,178 @@ +//! Define the trait and datatypes necessary to describe an instrument. + +use crate::{error::Result, usbtmc::UsbtmcAddr, InstrumentError}; +use std::{ + fmt::Display, + io::{Read, Write}, + net::SocketAddr, + time::Duration, +}; + +/// A generic connection address that covers all the different connection types. +/// Each device interface type will also have a [`TryFrom`] impl that converts from +/// this enum to itself. [`From`] is **not** implemented because the conversion could +/// fail. +#[non_exhaustive] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum ConnectionAddr { + /// A LAN connection is created with a [`SocketAddr`], which includes an IpAddr and + /// a port for the connection. + Lan(SocketAddr), + + /// A USBTMC connection is created with a [`UsbtmcAddr`]. + Usbtmc(UsbtmcAddr), + //Add other device interface types here + Unknown, +} + +impl Display for ConnectionAddr { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let s = match self { + Self::Lan(lan_info) => lan_info.to_string(), + Self::Usbtmc(usb_info) => usb_info.to_string(), + Self::Unknown => "".to_string(), + }; + write!(f, "{s}") + } +} + +/// The information about an instrument. +#[allow(clippy::module_name_repetitions)] +#[derive(serde::Serialize, Debug, Clone, PartialEq, Eq, Hash)] +pub struct InstrumentInfo { + /// The human-readable name of the vendor that makes the instrument + pub vendor: Option, + /// The model of the instrument + pub model: Option, + /// The serial number of the instrument + pub serial_number: Option, + /// The firmware revision of the instrument. + pub firmware_rev: Option, + /// The [`ConnectionAddr`] of the instrument. + #[serde(skip)] + pub address: Option, +} + +/// Get the [`InstrumentInfo`] from the given object that implements [`Read`] and +/// [`Write`]. +/// +/// # Errors +/// The following error classes can occur: +/// - Any [`std::io::Error`] that can occur with a [`Read`] or [`Write`] call +/// - Any error in converting the retrieved IDN string into [`InstrumentInfo`] +#[allow(clippy::module_name_repetitions)] +pub fn get_info(rw: &mut T) -> Result { + rw.write_all(b"*IDN?\n")?; + let mut info: Option = None; + for _ in 0..100 { + std::thread::sleep(Duration::from_millis(100)); + + let mut buf = vec![0u8; 100]; + let _ = rw.read(&mut buf)?; + let first_null = buf.iter().position(|&x| x == b'\0').unwrap_or(buf.len()); + let buf = &buf[..first_null]; + if let Ok(i) = buf.try_into() { + info = Some(i); + break; + } + } + info.ok_or(InstrumentError::InformationRetrievalError { + details: "unable to read instrument info".to_string(), + }) +} + +/// A trait to get the information from an instrument. +pub trait Info: Read + Write { + /// Get the information for the instrument. + /// + /// # Errors + /// [`TeaspoonInstrumentError::InformationRetrievalError`] if an instrument did not + /// return the requested information. + fn info(&mut self) -> Result { + get_info(self) + } +} + +impl TryFrom<&[u8]> for InstrumentInfo { + type Error = InstrumentError; + + fn try_from(idn: &[u8]) -> std::result::Result { + let parts: Vec<&[u8]> = idn + .split(|c| *c == b',' || *c == b'\n' || *c == b'\0') + .collect(); + + let (vendor, model, serial_number, firmware_rev) = match &parts[..] { + &[v, m, s, f, ..] => { + let fw_rev = String::from_utf8_lossy(f) + .to_string() + .trim_end_matches(|c| c == char::from(0)) + .trim() + .to_string(); + ( + Some(String::from_utf8_lossy(v).to_string()), + String::from_utf8_lossy(m) + .to_string() + .split("MODEL ") + .last() + .map(std::string::ToString::to_string), + Some(String::from_utf8_lossy(s).to_string()), + Some(fw_rev), + ) + } + _ => { + return Err(InstrumentError::InformationRetrievalError { + details: "unable to parse instrument information".to_string(), + }); + } + }; + + if model.is_none() { + return Err(InstrumentError::InformationRetrievalError { + details: "unable to parse model".to_string(), + }); + } + + Ok(Self { + vendor, + model, + serial_number, + firmware_rev, + address: None, + }) + } +} + +impl Display for InstrumentInfo { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let vendor = self.vendor.as_ref().map_or_else( + || String::from(""), + std::clone::Clone::clone, + ); + + let model: String = self + .model + .as_ref() + .map_or_else(|| String::from(""), std::clone::Clone::clone); + + let sn: String = self.serial_number.as_ref().map_or_else( + || String::from(""), + std::clone::Clone::clone, + ); + + let fw_rev = self.firmware_rev.as_ref().map_or_else( + || String::from(""), + std::clone::Clone::clone, + ); + + let addr = self + .address + .as_ref() + .map_or_else(|| ConnectionAddr::Unknown, std::clone::Clone::clone); + + if addr == ConnectionAddr::Unknown { + write!(f, "{vendor},MODEL {model},{sn},{fw_rev}") + } else { + write!(f, "{vendor},MODEL {model},{sn},{fw_rev},{addr}") + } + } +} diff --git a/src/instrument/language.rs b/src/instrument/language.rs new file mode 100644 index 0000000..1c2f735 --- /dev/null +++ b/src/instrument/language.rs @@ -0,0 +1,63 @@ +//! All of the enums, traits, and impls for language management on an instrument. + +use std::{fmt::Display, str::FromStr}; + +use crate::InstrumentError; + +/// The languages that could be on an instrument. +#[allow(clippy::module_name_repetitions)] +#[derive(Debug, PartialEq, Eq)] +pub enum CmdLanguage { + /// The SCPI language + Scpi, + /// The TSP language + Tsp, +} + +impl FromStr for CmdLanguage { + type Err = InstrumentError; + + fn from_str(s: &str) -> Result { + let s = s.trim_matches(|c| c == char::from(0)).trim(); + match s { + "SCPI" => Ok(Self::Scpi), + "TSP" => Ok(Self::Tsp), + _ => Err(InstrumentError::UnknownLanguage { + lang: s.trim().to_string(), + }), + } + } +} + +impl Display for CmdLanguage { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Scpi => write!(f, "SCPI"), + Self::Tsp => write!(f, "TSP"), + } + } +} + +/// The functions to interface with an instrument and check or set the language. +/// +/// # Default +/// The default implementation of this trait assumes that the instrument only has the +/// TSP language, which is true for all instruments except TTI, as of the writing of +/// this comment) +pub trait Language { + /// Get the current language on the instrument. + /// + /// # Errors + /// [`InstrumentError`] is returned in the case of IO error, Unknown Language or other errors + fn get_language(&mut self) -> Result { + Ok(CmdLanguage::Tsp) + } + + /// Set the language on the instrument to the given language. + /// + /// # Errors + /// [`InstrumentError`] is returned in the case of IO error, Unknown Language or other errors + fn change_language(&mut self, _lang: CmdLanguage) -> Result<(), InstrumentError> { + Ok(()) + } +} diff --git a/src/instrument/login.rs b/src/instrument/login.rs new file mode 100644 index 0000000..771ecf9 --- /dev/null +++ b/src/instrument/login.rs @@ -0,0 +1,96 @@ +//! The login functionality for an instrument. + +use crate::error::Result; + +/// The log-in state of an instrument. +#[derive(Debug, PartialEq, PartialOrd, Eq)] +pub enum State { + /// The instrument requires a login action for further communication. + Needed, + /// The instrument does _not_ require a login for further communication. Either the + /// instrument is not password protected OR the instrument is already unlocked for + /// this client. + NotNeeded, +} + +/// A trait that provides the expected functionality for logging into an instrument. +/// +/// This trait is required for a struct to be considered an instrument. +/// +/// Some instruments may not have the concept of logging in, for those instruments, +/// simply `impl Login` without any function definitions. +/// +/// # Examples +/// ## No Login Functionality Needed +/// ```no_run +/// use tsp_instrument::{instrument::{Login, State}, InstrumentError}; +/// +/// //This instrument does not have logins +/// struct ExampleInstrument { +/// //... +/// } +/// +/// impl Login for ExampleInstrument {} +/// ``` +/// +/// ## Login Functionality Needed +/// ```no_run +/// use tsp_instrument::{instrument::{Login, State}, InstrumentError}; +/// +/// struct Example { +/// logged_in: bool, +/// } +/// +/// impl Example { +/// fn enter_password(&mut self, token: &[u8]) -> Result<(), InstrumentError>{ +/// //... +/// self.logged_in = true; +/// Ok(()) +/// } +/// } +/// +/// impl Login for Example { +/// fn check_login(&mut self) -> Result { +/// if self.logged_in { +/// Ok(State::Needed) +/// } else { +/// Ok(State::NotNeeded) +/// } +/// } +/// +/// fn login(&mut self, token: &[u8]) -> Result<(), InstrumentError> +/// { +/// self.enter_password(token.as_ref()) +/// } +/// } +/// ``` +/// +pub trait Login { + /// Check the instrument to see if we need to login it. + /// + /// # Returns + /// - [`State::Needed`]: A login is necessary, therefore [`Login::login`] should be called. + /// - [`State::NotNeeded`]: A login is not necessary. Proceed with connection. + /// + /// # Default `impl` + /// The default implementation will always return [`State::NotNeeded`] and should + /// thus _not_ gate a connection to the instrument. + /// + /// # Errors + /// Returns an [`InstrumentError`] if any errors occurred. + fn check_login(&mut self) -> Result { + Ok(State::NotNeeded) + } + + /// Pass the given token to the instrument for it to authenticate. + /// + /// # Default `impl` + /// The default implementation will always return `Ok(())` and should therefore never + /// gate a connection to the instrument if called spuriously. + /// + /// # Errors + /// Returns an [`InstrumentError`] if any errors occurred. + fn login(&mut self, _token: &[u8]) -> Result<()> { + Ok(()) + } +} diff --git a/src/instrument/mod.rs b/src/instrument/mod.rs new file mode 100644 index 0000000..b074696 --- /dev/null +++ b/src/instrument/mod.rs @@ -0,0 +1,66 @@ +//! Trait definitions that need to be satisfied for any instrument. + +pub mod firmware; +pub mod info; +pub mod language; +pub mod login; +pub mod script; + +use std::{ + io::{Read, Write}, + time::Duration, +}; + +pub use firmware::Flash; +pub use info::Info; +pub use language::{CmdLanguage, Language}; +pub use login::{Login, State}; +pub use script::Script; + +use crate::interface::NonBlock; +use crate::{error::Result, InstrumentError}; + +/// A marker trait that defines the traits any [`Instrument`] needs to have. +pub trait Instrument: Flash + Info + Language + Login + Script + Read + Write + NonBlock {} + +/// Read from a 'rw' until we are sure we have cleared the output queue. +/// +/// # Errors +/// Whatever can errors can occur with [`std::io::Read`], [`std::io::Write`] or +/// [`tsp-instrument::interface::NonBlock`]. +pub fn clear_output_queue( + rw: &mut T, + max_attempts: usize, + delay_between_attempts: Duration, +) -> Result<()> { + let timestamp = chrono::Utc::now().to_string(); + + rw.write_all(format!("print(\"{timestamp}\")\n").as_bytes())?; + + rw.set_nonblocking(true)?; + + let mut accumulate = String::new(); + for _ in 0..max_attempts { + std::thread::sleep(delay_between_attempts); + let mut buf: Vec = vec![0u8; 512]; + match rw.read(&mut buf) { + Ok(_) => Ok(()), + Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock => { + std::thread::sleep(delay_between_attempts); + continue; + } + Err(e) => Err(e), + }?; + let first_null = buf.iter().position(|&x| x == b'\0').unwrap_or(buf.len()); + let buf = &buf[..first_null]; + if !buf.is_empty() { + accumulate = format!("{accumulate}{}", String::from_utf8_lossy(buf)); + } + if accumulate.contains(×tamp) { + return Ok(()); + } + } + Err(InstrumentError::Other( + "unable to clear instrument output queue".to_string(), + )) +} diff --git a/src/instrument/script.rs b/src/instrument/script.rs new file mode 100644 index 0000000..597f286 --- /dev/null +++ b/src/instrument/script.rs @@ -0,0 +1,67 @@ +//! A trait that allows for the writing of a TSP script file to the instrument. + +use std::io::{BufRead, Write}; + +use bytes::Buf; + +use crate::error::Result; + +/// The [`Instrument`] can write a script to be executed. +pub trait Script +where + Self: Write, +{ + /// Write the given script to the instrument with the given name. + /// + /// # Parameters + /// - `name` - is the environment-compatible name of the script + /// - `script` - is the contents of the script + /// - `save_script` - `true` if the script should be saved to non-volatile memory + /// - `run_script` - `true` if the script should be run after load + /// + /// # Notes + /// - The script name will not be validated to ensure that it is compatible with the + /// scripting environment. + /// - The given script content will only be validated by the instrument, but not + /// the [`write_script`] function. + /// + /// # Errors + /// Returns an [`InstrumentError`] if any errors occurred. + fn write_script( + &mut self, + name: &[u8], + script: &[u8], + save_script: bool, + run_script: bool, + ) -> Result<()> { + let name = String::from_utf8_lossy(name).to_string(); + let script = script.reader(); //String::from_utf8_lossy(script.as_ref()).to_string(); + self.write_all(b"_orig_prompts = localnode.prompts localnode.prompts = 0\n")?; + self.flush()?; + self.write_all(format!("{name}=nil\n").as_bytes())?; + self.flush()?; + self.write_all(format!("loadscript {name}\n").as_bytes())?; + + for line in script.lines() { + self.write_all(format!("{}\n", line?).as_bytes())?; + } + + self.write_all(b"\nendscript\n")?; + self.flush()?; + + if save_script { + self.write_all(format!("{name}.save()\n").as_bytes())?; + self.flush()?; + } + + if run_script { + self.write_all(format!("{name}.run()\n").as_bytes())?; + self.flush()?; + } + + self.write_all(b"localnode.prompts = _orig_prompts _orig_prompts = nil\n")?; + self.flush()?; + + Ok(()) + } +} diff --git a/src/interface/async_stream.rs b/src/interface/async_stream.rs new file mode 100644 index 0000000..e43122c --- /dev/null +++ b/src/interface/async_stream.rs @@ -0,0 +1,170 @@ +use std::{ + io::{ErrorKind, Read, Write}, + rc::Rc, + sync::{ + mpsc::{self, Receiver, Sender, TryRecvError}, + Arc, + }, + thread::JoinHandle, + time::Duration, +}; + +use crate::error::{InstrumentError, Result}; + +use crate::interface::{Interface, NonBlock}; + +//create an Async version of the interface +pub struct AsyncStream { + join: JoinHandle>>, + write_to: Sender>, + read_from: Rc>>, + buffer: Vec, + nonblocking: bool, +} + +impl TryFrom> for AsyncStream { + type Error = InstrumentError; + + fn try_from( + mut socket: Arc, + ) -> std::result::Result { + let (write_to, read_into) = mpsc::channel(); + let (write_out, read_from) = mpsc::channel(); + let builder = + std::thread::Builder::new().name("Instrument Communication Thread".to_string()); + //TODO: Populate name with instrument information + + let join = builder.spawn(move || -> Result> { + Arc::get_mut(&mut socket).unwrap().set_nonblocking(true)?; + let read_into: Receiver> = read_into; + let write_out: Sender> = write_out; + + 'rw_loop: loop { + // see if the application has anything to send to the instrument. + std::thread::sleep(Duration::from_micros(1)); + match read_into.try_recv() { + // It does, so send it + Ok(msg) => match Arc::get_mut(&mut socket) + .unwrap() + // Do NOT add a newline here. It is added elsewhere. + .write_all(format!("{}", String::from_utf8_lossy(&msg)).as_bytes()) + { + Ok(()) => {} + Err(e) => { + // There was an Error sending to the instrument. + // clean up and get out. + return Err(e.into()); + } + }, + Err(e) => match e { + // The sender has disconnected, therefore we need to clean up + mpsc::TryRecvError::Disconnected => break 'rw_loop, + mpsc::TryRecvError::Empty => {} + }, + } + let buf = &mut [0u8; 512]; + if let Ok(size) = Arc::get_mut(&mut socket).unwrap().read(buf) { + let buf = &buf[..size]; + // This `send()` sends this to a receiver that will be activated next time a self.read() is called. + if size > 0 && write_out.send(buf.to_vec()).is_err() { + return Err(std::io::Error::new( + ErrorKind::ConnectionReset, + "attempted to send message from device, but client was closed" + .to_string(), + ) + .into()); + } + } + } + Ok(socket) + })?; + + Ok(Self { + join, + write_to, + read_from: Rc::new(read_from), + buffer: Vec::new(), + nonblocking: true, + }) + } +} + +impl Read for AsyncStream { + fn read(&mut self, buf: &mut [u8]) -> std::io::Result { + // define an error since all of the returned errors are for the same reason. + let error: std::io::Error = std::io::Error::new( + ErrorKind::NotConnected, + "attempted to read asynchronously from socket, but it was not connected".to_string(), + ); + + let resp = if self.nonblocking { + match self.read_from.try_recv() { + Ok(resp) => resp, + Err(e) => match e { + TryRecvError::Empty => Vec::default(), + TryRecvError::Disconnected => { + return Err(error); + } + }, + } + } else { + if !self.buffer.is_empty() { + let read_size = self.buffer.take(buf.len() as u64).read(buf)?; + self.buffer = self.buffer[read_size..].into(); + return Ok(read_size); + } + match self.read_from.recv() { + Ok(resp) => resp, + Err(_) => { + return Err(error); + } + } + }; + + let _ = self.buffer.write(&resp); + let read_size = self.buffer.take(buf.len() as u64).read(buf)?; + self.buffer = self.buffer[read_size..].into(); + + Ok(read_size) + } +} + +impl Write for AsyncStream { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + let Ok(()) = self.write_to.send(Vec::from(buf)) else { + return Err(std::io::Error::new( + ErrorKind::NotConnected, + "attempted to write asynchronously to socket, but it was not connected".to_string(), + )); + }; + + Ok(buf.len()) + } + + fn flush(&mut self) -> std::io::Result<()> { + Ok(()) + } +} + +impl NonBlock for AsyncStream { + fn set_nonblocking(&mut self, nonblocking: bool) -> Result<()> { + self.nonblocking = nonblocking; + Ok(()) + } +} + +impl TryFrom for Arc { + type Error = InstrumentError; + + fn try_from(async_stream: AsyncStream) -> std::result::Result { + drop(async_stream.write_to); + match async_stream.join.join() { + Ok(Ok(stream)) => Ok(stream), + _ => Err(InstrumentError::ConnectionError { + details: "unable to retrieve synchronous stream".to_string(), + }), + } + } +} + +impl Interface for AsyncStream {} diff --git a/src/interface/connection_addr.rs b/src/interface/connection_addr.rs new file mode 100644 index 0000000..31584bf --- /dev/null +++ b/src/interface/connection_addr.rs @@ -0,0 +1,32 @@ +use std::fmt::Display; +use std::net::SocketAddr; + +use crate::interface::usbtmc::UsbtmcAddr; + +/// A generic connection address that covers all the different connection types. +/// Each device interface type will also have a [`TryFrom`] impl that converts from +/// this enum to itself. [`From`] is **not** implemented because the conversion could +/// fail. +#[non_exhaustive] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum ConnectionAddr { + /// A LAN connection is created with a [`SocketAddr`], which includes an IpAddr and + /// a port for the connection. + Lan(SocketAddr), + + /// A USBTMC connection is created with a [`UsbtmcAddr`]. + Usbtmc(UsbtmcAddr), + //Add other device interface types here + Unknown, +} + +impl Display for ConnectionAddr { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let s = match self { + Self::Lan(lan_info) => lan_info.to_string(), + Self::Usbtmc(usb_info) => usb_info.to_string(), + Self::Unknown => "".to_string(), + }; + write!(f, "{s}") + } +} diff --git a/src/interface/mod.rs b/src/interface/mod.rs new file mode 100644 index 0000000..ccfe6fe --- /dev/null +++ b/src/interface/mod.rs @@ -0,0 +1,31 @@ +use std::{ + io::{Read, Write}, + net::TcpStream, +}; + +use crate::error::Result; + +pub mod async_stream; +pub mod connection_addr; +pub mod usbtmc; + +/// Defines a marker trait that we will implement on each device interface +pub trait Interface: NonBlock + Read + Write {} + +/// This device can be set to be non-blocking. This is a requirement of an Interface +pub trait NonBlock { + /// Set the interface not to block on reads to allow for polling. + /// + /// # Errors + /// There may be errors that occur from the associated physical interface + /// (e.g. LAN, USB). + fn set_nonblocking(&mut self, enable: bool) -> Result<()>; +} + +impl NonBlock for TcpStream { + fn set_nonblocking(&mut self, enable: bool) -> crate::error::Result<()> { + Ok(Self::set_nonblocking(self, enable)?) + } +} + +impl Interface for TcpStream {} diff --git a/src/interface/usbtmc.rs b/src/interface/usbtmc.rs new file mode 100644 index 0000000..2f74dc4 --- /dev/null +++ b/src/interface/usbtmc.rs @@ -0,0 +1,174 @@ +use std::{ + fmt::Display, + hash::Hash, + io::{ErrorKind, Read, Write}, + str::FromStr, + time::Duration, +}; + +use rusb::{Context, DeviceList}; +use tmc::InstrumentHandle; + +use crate::{error::Result, interface, interface::NonBlock, InstrumentError}; + +const KEITHLEY_VID: u16 = 0x05e6; + +/// An address representing how to connect to a USBTMC device. +#[allow(clippy::module_name_repetitions)] +#[derive(Debug, Clone)] +pub struct UsbtmcAddr { + pub device: rusb::Device, + pub model: String, + pub serial: String, +} + +impl Hash for UsbtmcAddr { + fn hash(&self, state: &mut H) { + format!("{}:{}:{}", "USB", self.model, self.serial).hash(state); + } +} + +impl Eq for UsbtmcAddr {} + +impl PartialEq for UsbtmcAddr { + fn eq(&self, other: &Self) -> bool { + self.serial == other.serial && self.model == other.model + } +} + +impl Display for UsbtmcAddr { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "USB:{}:{}", self.model, self.serial) + } +} + +impl FromStr for UsbtmcAddr { + type Err = InstrumentError; + + fn from_str(s: &str) -> std::result::Result { + //USB:2450:01234567 + let split: Vec<&str> = s.split(':').collect(); + if split.len() != 3 { + return Err(InstrumentError::AddressParsingError { + unparsable_string: s.to_string(), + }); + } + + let (ix, model, serial) = (split[0], split[1], split[2]); + + if ix.trim() != "USB" { + return Err(InstrumentError::AddressParsingError { + unparsable_string: s.to_string(), + }); + } + let context = rusb::Context::new()?; + for device in DeviceList::new_with_context(context)?.iter() { + let desc = device.device_descriptor()?; + if desc.vendor_id() == KEITHLEY_VID + && desc.product_id() == u16::from_str_radix(model, 16)? + { + let handle = device.open()?; + let languages = handle.read_languages(Duration::from_millis(100))?; + let Some(language) = languages.first() else { + continue; + }; + let sn = handle.read_serial_number_string( + *language, + &desc, + Duration::from_millis(100), + )?; + if *serial == sn { + return Ok(Self { + device, + model: model.to_string(), + serial: serial.to_string(), + }); + } + } + } + + Err(InstrumentError::ConnectionError { + details: format!( + "USB device with model '{model}' and serial number '{serial}' could not be found" + ), + }) + } +} + +#[derive(Debug)] +pub struct Stream { + handle: InstrumentHandle, + nonblocking: bool, +} + +impl TryFrom for Stream { + type Error = InstrumentError; + + fn try_from(addr: UsbtmcAddr) -> std::result::Result { + Ok(Self { + handle: tmc::Instrument::new(addr.device)? + .ok_or(InstrumentError::ConnectionError { + details: "unable to connect to USB instrument".to_string(), + })? + .open()?, + nonblocking: false, + }) + } +} + +impl TryFrom> for Stream { + type Error = InstrumentError; + fn try_from(handle: InstrumentHandle) -> std::result::Result { + Ok(Self { + handle, + nonblocking: false, + }) + } +} + +impl NonBlock for Stream { + fn set_nonblocking(&mut self, enable: bool) -> Result<()> { + self.nonblocking = enable; + Ok(()) + } +} + +impl interface::Interface for Stream {} + +impl Write for Stream { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + self.handle.write_raw(buf)?; + Ok(buf.len()) + } + + fn flush(&mut self) -> std::io::Result<()> { + //Nothing to force-flush on USBTMC, handled by `write_raw` above + Ok(()) + } +} + +impl Read for Stream { + fn read(&mut self, buf: &mut [u8]) -> std::io::Result { + let message_available = if self.nonblocking { + self.handle.read_stb(Some(Duration::from_millis(10)))? + } else { + while !self.handle.read_stb(Some(Duration::from_millis(10)))? {} + true + }; + + if message_available { + let msg = self.handle.read_raw(Some(match u32::try_from(buf.len()) { + Ok(v) => v, + Err(e) => { + return Err(std::io::Error::new( + ErrorKind::Other, + format!("buffer larger than can be read: {e}"), + )); + } + }))?; + msg.take(buf.len() as u64).read(buf) + } else { + Ok(0) + } + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..fbc6d17 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,32 @@ +#![feature(lint_reasons, rustdoc_missing_doc_code_examples)] +#![deny( + clippy::undocumented_unsafe_blocks, + clippy::pedantic, + clippy::nursery, + clippy::arithmetic_side_effects +)] +#![feature(assert_matches)] +//#![warn(missing_docs, rustdoc::all)] +//#![allow(rustdoc::missing_doc_code_examples)] +#![doc(html_logo_url = "../../../ki-comms_doc_icon.png")] + +//! The TSP Instrument crate defines the necessary components to enable communication +//! with an instrument at various levels of abstraction. The aim of the library is to +//! interact correctly, efficiently and reliably with Keithley instruments via multiple +//! connection interfaces. Right now LAN and USBTMC are implemented, and VISA is +//! planned + +//pub mod connect; +pub mod error; +pub mod instrument; +pub mod interface; +pub mod model; + +#[cfg(test)] +pub(crate) mod test_util; + +//pub use connect::{AsyncToSync, Connect, ConnectAsync, ConnectionAddr, Disconnect}; +pub use error::InstrumentError; +pub use instrument::firmware::Flash; +pub use interface::{connection_addr::ConnectionAddr, usbtmc, Interface}; +pub use model::{ki2600, tti, versatest}; diff --git a/src/model/ki2600.rs b/src/model/ki2600.rs new file mode 100644 index 0000000..a4077d0 --- /dev/null +++ b/src/model/ki2600.rs @@ -0,0 +1,980 @@ +use std::io::{BufRead, Read, Write}; + +use bytes::Buf; + +use crate::{ + instrument::{self, info::InstrumentInfo, language, Info, Login, Script}, + interface::Interface, + interface::NonBlock, + Flash, InstrumentError, +}; + +pub struct Instrument { + info: Option, + interface: Box, +} + +impl Instrument { + #[must_use] + pub fn is(info: &InstrumentInfo) -> bool { + info.model.as_ref().map_or(false, |model| { + model.split_ascii_whitespace().last().map_or(false, is_2600) + }) + } + + #[must_use] + pub const fn new(interface: Box) -> Self { + Self { + info: None, + interface, + } + } + + pub fn add_info(&mut self, info: InstrumentInfo) -> &Self { + self.info = Some(info); + self + } +} + +//Implement device_interface::Interface since it is a subset of instrument::Instrument trait. +impl instrument::Instrument for Instrument {} + +fn is_2600(model: impl AsRef) -> bool { + [ + "2601", + "2602", + "2611", + "2612", + "2635", + "2636", + "2601A", + "2602A", + "2611A", + "2612A", + "2635A", + "2636A", + "2651A", + "2657A", + "2601B", + "2601B-PULSE", + "2602B", + "2606B", + "2611B", + "2612B", + "2635B", + "2636B", + "2604B", + "2614B", + "2634B", + "2601B-L", + "2602B-L", + "2611B-L", + "2612B-L", + "2635B-L", + "2636B-L", + "2604B-L", + "2614B-L", + "2634B-L", + "3706", + "3706-SNFP", + "3706-S", + "3706-NFP", + "3706A", + "3706A-SNFP", + "3706A-S", + "3706A-NFP", + "707B", + "708B", + ] + .contains(&model.as_ref()) +} + +impl Info for Instrument {} + +impl language::Language for Instrument {} + +impl Login for Instrument { + fn check_login(&mut self) -> crate::error::Result { + self.write_all(b"print('unlocked')\n")?; + + let mut resp: Vec = vec![0; 256]; + let _read = self.read(&mut resp)?; + + let resp = std::str::from_utf8(resp.as_slice()) + .unwrap_or("") + .trim_matches(char::from(0)) + .trim(); + + if resp.contains("FAILURE") { + Ok(instrument::State::Needed) + } else { + Ok(instrument::State::NotNeeded) + } + } + + fn login(&mut self, token: &[u8]) -> crate::error::Result<()> { + if instrument::State::NotNeeded == self.check_login()? { + return Ok(()); + } + + self.write_all(format!("password {}\n", String::from_utf8_lossy(token)).as_bytes())?; + + if instrument::State::Needed == self.check_login()? { + return Err(InstrumentError::LoginRejected); + } + + Ok(()) + } +} + +impl Script for Instrument {} + +impl Flash for Instrument { + fn flash_firmware(&mut self, image: &[u8], _: Option) -> crate::error::Result<()> { + let image = image.reader(); + self.write_all(b"flash\n")?; + + for line in image.lines() { + self.write_all(format!("{}\n", line?).as_bytes())?; + } + + self.write_all(b"endflash\n")?; + Ok(()) + } +} + +impl Read for Instrument { + fn read(&mut self, buf: &mut [u8]) -> std::io::Result { + self.interface.read(buf) + } +} + +impl Write for Instrument { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + self.interface.write(buf) + } + + fn flush(&mut self) -> std::io::Result<()> { + self.interface.flush() + } +} + +impl NonBlock for Instrument { + fn set_nonblocking(&mut self, enable: bool) -> crate::error::Result<()> { + self.interface.set_nonblocking(enable) + } +} + +impl Drop for Instrument { + fn drop(&mut self) { + let _ = self.interface.write_all(b"abort\n"); + } +} + +#[cfg(test)] +mod unit { + use std::{ + assert_matches::assert_matches, + io::{BufRead, Read, Write}, + }; + + use bytes::Buf; + use mockall::{mock, Sequence}; + + use crate::{ + instrument::{self, info::Info, Login, Script}, + interface::NonBlock, + interface::{self}, + test_util, Flash, InstrumentError, + }; + + use super::Instrument; + + #[test] + fn login_not_needed() { + let mut interface = MockInterface::new(); + + let mut seq = Sequence::new(); + + // A successful login attempt on a TTI instrument is as follows: + // 1. Instrument connects to interface + // 2. Instrument sends "*STB?\n" + // 3. Instrument reads from interface and receives status byte + // 4. Instrument returns `instrument::State::NotNeeded` + + interface + .expect_write() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf == b"print('unlocked')\n") + .returning(|buf: &[u8]| Ok(buf.len())); + + interface + .expect_read() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf.len() >= 2) + .return_once(|buf: &mut [u8]| { + let msg = b"unlocked\n"; + if buf.len() >= msg.len() { + let bytes = msg[..] + .reader() + .read(buf) + .expect("MockInterface should write to buffer"); + assert_eq!(bytes, msg.len()); + } + Ok(msg.len()) + }); + + interface + .expect_write() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf == b"print('unlocked')\n") + .returning(|buf: &[u8]| Ok(buf.len())); + + interface + .expect_read() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf.len() >= 2) + .return_once(|buf: &mut [u8]| { + let msg = b"unlocked\n"; + if buf.len() >= msg.len() { + let bytes = msg[..] + .reader() + .read(buf) + .expect("MockInterface should write to buffer"); + assert_eq!(bytes, msg.len()); + } + Ok(msg.len()) + }); + + interface + .expect_write() + .times(..) + .withf(|buf: &[u8]| buf == b"abort\n") + .returning(|buf: &[u8]| Ok(buf.len())); + + let mut instrument: Instrument = Instrument::new(Box::new(interface)); + + assert_matches!(instrument.check_login(), Ok(instrument::State::NotNeeded)); + + assert!(instrument.login(b"secret_token").is_ok()); + } + + #[test] + #[allow(clippy::too_many_lines)] //Allow for now. + fn login_success() { + let mut _interface = MockInterface::new(); + let mut interface = MockInterface::new(); + + let mut seq = Sequence::new(); + + // check_login() + interface + .expect_write() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf == b"print('unlocked')\n") + .returning(|buf: &[u8]| Ok(buf.len())); + + interface + .expect_read() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf.len() >= 8) + .return_once(|buf: &mut [u8]| { + let msg = b"FAILURE\n"; + if buf.len() >= msg.len() { + let bytes = msg[..] + .reader() + .read(buf) + .expect("MockInstrument should write to buffer"); + assert_eq!(bytes, msg.len()); + } + Ok(msg.len()) + }); + + // login() { first check_login() } + interface + .expect_write() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf == b"print('unlocked')\n") + .returning(|buf: &[u8]| Ok(buf.len())); + + interface + .expect_read() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf.len() >= 8) + .return_once(|buf: &mut [u8]| { + let msg = b"FAILURE\n"; + if buf.len() >= msg.len() { + let bytes = msg[..] + .reader() + .read(buf) + .expect("MockInstrument should write to buffer"); + assert_eq!(bytes, msg.len()); + } + Ok(msg.len()) + }); + + // login() {write(b"login {token}")} + interface + .expect_write() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf == b"password secret_token\n") + .returning(|buf: &[u8]| Ok(buf.len())); + + // login() { second check_login() } + interface + .expect_write() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf == b"print('unlocked')\n") + .returning(|buf: &[u8]| Ok(buf.len())); + + interface + .expect_read() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf.len() >= 8) + .return_once(|buf: &mut [u8]| { + let msg = b"unlocked\n"; + if buf.len() >= msg.len() { + let bytes = msg[..] + .reader() + .read(buf) + .expect("MockInstrument should write to buffer"); + assert_eq!(bytes, msg.len()); + } + Ok(msg.len()) + }); + + // check_login() + interface + .expect_write() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf == b"print('unlocked')\n") + .returning(|buf: &[u8]| Ok(buf.len())); + + interface + .expect_read() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf.len() >= 8) + .return_once(|buf: &mut [u8]| { + let msg = b"unlocked\n"; + if buf.len() >= msg.len() { + let bytes = msg[..] + .reader() + .read(buf) + .expect("MockInstrument should write to buffer"); + assert_eq!(bytes, msg.len()); + } + Ok(msg.len()) + }); + + interface + .expect_write() + .times(..) + .withf(|buf: &[u8]| buf == b"abort\n") + .returning(|buf: &[u8]| Ok(buf.len())); + + let mut instrument: Instrument = Instrument::new(Box::new(interface)); + + assert_matches!(instrument.check_login(), Ok(instrument::State::Needed)); + + assert_matches!(instrument.login(b"secret_token"), Ok(())); + + assert_matches!(instrument.check_login(), Ok(instrument::State::NotNeeded)); + } + + #[test] + #[allow(clippy::too_many_lines)] //Allow for now + fn login_failure() { + let mut _interface = MockInterface::new(); + let mut interface = MockInterface::new(); + + let mut seq = Sequence::new(); + + // check_login() + interface + .expect_write() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf == b"print('unlocked')\n") + .returning(|buf: &[u8]| Ok(buf.len())); + + interface + .expect_read() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf.len() >= 8) + .return_once(|buf: &mut [u8]| { + let msg = b"FAILURE\n"; + if buf.len() >= msg.len() { + let bytes = msg[..] + .reader() + .read(buf) + .expect("MockInstrument should write to buffer"); + assert_eq!(bytes, msg.len()); + } + Ok(msg.len()) + }); + + // login() { first check_login() } + interface + .expect_write() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf == b"print('unlocked')\n") + .returning(|buf: &[u8]| Ok(buf.len())); + + interface + .expect_read() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf.len() >= 8) + .return_once(|buf: &mut [u8]| { + let msg = b"FAILURE\n"; + if buf.len() >= msg.len() { + let bytes = msg[..] + .reader() + .read(buf) + .expect("MockInstrument should write to buffer"); + assert_eq!(bytes, msg.len()); + } + Ok(msg.len()) + }); + + // login() {write(b"login {token}")} + interface + .expect_write() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf == b"password secret_token\n") + .returning(|buf: &[u8]| Ok(buf.len())); + + // login() { second check_login() } + interface + .expect_write() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf == b"print('unlocked')\n") + .returning(|buf: &[u8]| Ok(buf.len())); + + interface + .expect_read() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf.len() >= 8) + .return_once(|buf: &mut [u8]| { + let msg = b"FAILURE\n"; + if buf.len() >= msg.len() { + let bytes = msg[..] + .reader() + .read(buf) + .expect("MockInstrument should write to buffer"); + assert_eq!(bytes, msg.len()); + } + Ok(msg.len()) + }); + + // check_login() + interface + .expect_write() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf == b"print('unlocked')\n") + .returning(|buf: &[u8]| Ok(buf.len())); + + interface + .expect_read() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf.len() >= 8) + .return_once(|buf: &mut [u8]| { + let msg = b"FAILURE\n"; + if buf.len() >= msg.len() { + let bytes = msg[..] + .reader() + .read(buf) + .expect("MockInstrument should write to buffer"); + assert_eq!(bytes, msg.len()); + } + Ok(msg.len()) + }); + + interface + .expect_write() + .times(..) + .withf(|buf: &[u8]| buf == b"abort\n") + .returning(|buf: &[u8]| Ok(buf.len())); + let mut instrument: Instrument = Instrument::new(Box::new(interface)); + + assert_matches!(instrument.check_login(), Ok(instrument::State::Needed)); + + assert_matches!( + instrument.login(b"secret_token"), + Err(InstrumentError::LoginRejected) + ); + + assert_matches!(instrument.check_login(), Ok(instrument::State::Needed)); + } + + #[test] + fn info() { + let mut _interface = MockInterface::new(); + let mut interface = MockInterface::new(); + + let mut seq = Sequence::new(); + + // check_login() + interface + .expect_write() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf == b"*IDN?\n") + .returning(|buf: &[u8]| Ok(buf.len())); + + interface + .expect_read() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf.len() >= 50) + .return_once(|buf: &mut [u8]| { + let msg = b"KEITHLEY INSTRUMENTS,MODEL 2636B,0123456789,1.2.3d\n"; + if buf.len() >= msg.len() { + let bytes = msg[..] + .reader() + .read(buf) + .expect("MockInterface should write to buffer"); + assert_eq!(bytes, msg.len()); + } + Ok(msg.len()) + }); + interface + .expect_write() + .times(..) + .withf(|buf: &[u8]| buf == b"abort\n") + .returning(|buf: &[u8]| Ok(buf.len())); + + let mut instrument: Instrument = Instrument::new(Box::new(interface)); + + let info = instrument + .info() + .expect("instrument can get instrument information from MockInterface"); + + let exp_vendor = "KEITHLEY INSTRUMENTS".to_string(); + let exp_model = "2636B".to_string(); + let exp_serial = "0123456789".to_string(); + let exp_fw = "1.2.3d".to_string(); + + assert_eq!(info.vendor.unwrap(), exp_vendor); + assert_eq!(info.model.unwrap(), exp_model); + assert_eq!(info.serial_number.unwrap(), exp_serial); + assert_eq!(info.firmware_rev.unwrap(), exp_fw); + } + + #[test] + fn write_script() { + let mut interface = MockInterface::new(); + + let mut seq = Sequence::new(); + + interface + .expect_write() + .times(..) + .withf(|buf: &[u8]| buf == b"_orig_prompts = localnode.prompts localnode.prompts = 0\n") + .returning(|buf: &[u8]| Ok(buf.len())); + + interface + .expect_write() + .times(..) + .withf(|buf: &[u8]| buf == b"localnode.prompts = _orig_prompts _orig_prompts = nil\n") + .returning(|buf: &[u8]| Ok(buf.len())); + + //Accept any number of flushes + interface.expect_flush().times(..).returning(|| Ok(())); + + interface + .expect_write() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf == b"test_script=nil\n") + .returning(|buf: &[u8]| Ok(buf.len())); + + interface + .expect_write() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf == b"loadscript test_script\n") + .returning(|buf: &[u8]| Ok(buf.len())); + + interface + .expect_write() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf == b"line1\n") + .returning(|buf: &[u8]| Ok(buf.len())); + + interface + .expect_write() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf == b"line2\n") + .returning(|buf: &[u8]| Ok(buf.len())); + + interface + .expect_write() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf == b"line3\n") + .returning(|buf: &[u8]| Ok(buf.len())); + + interface + .expect_write() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf == b"\nendscript\n") + .returning(|buf: &[u8]| Ok(buf.len())); + + interface + .expect_write() + .times(..) + .withf(|buf: &[u8]| buf == b"abort\n") + .returning(|buf: &[u8]| Ok(buf.len())); + + let mut instrument: Instrument = Instrument::new(Box::new(interface)); + + instrument + .write_script(b"test_script", &b"line1\nline2\nline3"[..], false, false) + .expect("instrument should have written script to MockInterface"); + } + + #[test] + fn write_script_run() { + let mut interface = MockInterface::new(); + + let mut seq = Sequence::new(); + + interface + .expect_write() + .times(..) + .withf(|buf: &[u8]| buf == b"_orig_prompts = localnode.prompts localnode.prompts = 0\n") + .returning(|buf: &[u8]| Ok(buf.len())); + + interface + .expect_write() + .times(..) + .withf(|buf: &[u8]| buf == b"localnode.prompts = _orig_prompts _orig_prompts = nil\n") + .returning(|buf: &[u8]| Ok(buf.len())); + + //Accept any number of flushes + interface.expect_flush().times(..).returning(|| Ok(())); + + interface + .expect_write() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf == b"test_script=nil\n") + .returning(|buf: &[u8]| Ok(buf.len())); + + interface + .expect_write() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf == b"loadscript test_script\n") + .returning(|buf: &[u8]| Ok(buf.len())); + + interface + .expect_write() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf == b"line1\n") + .returning(|buf: &[u8]| Ok(buf.len())); + + interface + .expect_write() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf == b"line2\n") + .returning(|buf: &[u8]| Ok(buf.len())); + + interface + .expect_write() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf == b"line3\n") + .returning(|buf: &[u8]| Ok(buf.len())); + + interface + .expect_write() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf == b"\nendscript\n") + .returning(|buf: &[u8]| Ok(buf.len())); + + interface + .expect_write() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf == b"test_script.run()\n") + .returning(|buf: &[u8]| Ok(buf.len())); + interface + .expect_write() + .times(..) + .withf(|buf: &[u8]| buf == b"abort\n") + .returning(|buf: &[u8]| Ok(buf.len())); + + let mut instrument: Instrument = Instrument::new(Box::new(interface)); + + instrument + .write_script(b"test_script", &b"line1\nline2\nline3"[..], false, true) + .expect("instrument should have written script to MockInterface"); + } + + #[test] + fn write_script_save() { + let mut interface = MockInterface::new(); + + let mut seq = Sequence::new(); + + interface + .expect_write() + .times(..) + .withf(|buf: &[u8]| buf == b"_orig_prompts = localnode.prompts localnode.prompts = 0\n") + .returning(|buf: &[u8]| Ok(buf.len())); + + interface + .expect_write() + .times(..) + .withf(|buf: &[u8]| buf == b"localnode.prompts = _orig_prompts _orig_prompts = nil\n") + .returning(|buf: &[u8]| Ok(buf.len())); + + //Accept any number of flushes + interface.expect_flush().times(..).returning(|| Ok(())); + + interface + .expect_write() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf == b"test_script=nil\n") + .returning(|buf: &[u8]| Ok(buf.len())); + + interface + .expect_write() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf == b"loadscript test_script\n") + .returning(|buf: &[u8]| Ok(buf.len())); + + interface + .expect_write() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf == b"line1\n") + .returning(|buf: &[u8]| Ok(buf.len())); + + interface + .expect_write() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf == b"line2\n") + .returning(|buf: &[u8]| Ok(buf.len())); + + interface + .expect_write() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf == b"line3\n") + .returning(|buf: &[u8]| Ok(buf.len())); + + interface + .expect_write() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf == b"\nendscript\n") + .returning(|buf: &[u8]| Ok(buf.len())); + + interface + .expect_write() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf == b"test_script.save()\n") + .returning(|buf: &[u8]| Ok(buf.len())); + interface + .expect_write() + .times(..) + .withf(|buf: &[u8]| buf == b"abort\n") + .returning(|buf: &[u8]| Ok(buf.len())); + + let mut instrument: Instrument = Instrument::new(Box::new(interface)); + + instrument + .write_script(b"test_script", &b"line1\nline2\nline3"[..], true, false) + .expect("instrument should have written script to MockInterface"); + } + + #[test] + fn write_script_save_run() { + let mut interface = MockInterface::new(); + + let mut seq = Sequence::new(); + interface + .expect_write() + .times(..) + .withf(|buf: &[u8]| buf == b"_orig_prompts = localnode.prompts localnode.prompts = 0\n") + .returning(|buf: &[u8]| Ok(buf.len())); + + interface + .expect_write() + .times(..) + .withf(|buf: &[u8]| buf == b"localnode.prompts = _orig_prompts _orig_prompts = nil\n") + .returning(|buf: &[u8]| Ok(buf.len())); + + //Accept any number of flushes + interface.expect_flush().times(..).returning(|| Ok(())); + + interface + .expect_write() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf == b"test_script=nil\n") + .returning(|buf: &[u8]| Ok(buf.len())); + + interface + .expect_write() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf == b"loadscript test_script\n") + .returning(|buf: &[u8]| Ok(buf.len())); + + interface + .expect_write() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf == b"line1\n") + .returning(|buf: &[u8]| Ok(buf.len())); + + interface + .expect_write() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf == b"line2\n") + .returning(|buf: &[u8]| Ok(buf.len())); + + interface + .expect_write() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf == b"line3\n") + .returning(|buf: &[u8]| Ok(buf.len())); + + interface + .expect_write() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf == b"\nendscript\n") + .returning(|buf: &[u8]| Ok(buf.len())); + + interface + .expect_write() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf == b"test_script.save()\n") + .returning(|buf: &[u8]| Ok(buf.len())); + + interface + .expect_write() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf == b"test_script.run()\n") + .returning(|buf: &[u8]| Ok(buf.len())); + + interface + .expect_write() + .times(..) + .withf(|buf: &[u8]| buf == b"abort\n") + .returning(|buf: &[u8]| Ok(buf.len())); + let mut instrument: Instrument = Instrument::new(Box::new(interface)); + + instrument + .write_script(b"test_script", &b"line1\nline2\nline3"[..], true, true) + .expect("instrument should have written script to MockInterface"); + } + + #[test] + fn flash_firmware() { + let mut interface = MockInterface::new(); + + let mut seq = Sequence::new(); + + interface + .expect_write() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf == b"flash\n") + .returning(|buf: &[u8]| Ok(buf.len())); + + for line in test_util::SIMPLE_FAKE_TEXTUAL_FW.reader().lines() { + interface + .expect_write() + .times(1) + .in_sequence(&mut seq) + .withf(move |buf: &[u8]| { + buf == format!( + "{}\n", + line.as_ref() + .expect("textual test firmware should return all Ok for lines()") + ) + .as_bytes() + }) + .returning(|buf: &[u8]| Ok(buf.len())); + } + + interface + .expect_write() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf == b"endflash\n") + .returning(|buf: &[u8]| Ok(buf.len())); + interface + .expect_write() + .times(..) + .withf(|buf: &[u8]| buf == b"abort\n") + .returning(|buf: &[u8]| Ok(buf.len())); + + let mut instrument: Instrument = Instrument::new(Box::new(interface)); + + instrument + .flash_firmware(test_util::SIMPLE_FAKE_TEXTUAL_FW, Some(0)) + .expect("instrument should have written fw to MockInterface"); + } + + // Define a mock interface to be used in the tests above. + mock! { + Interface {} + + impl interface::Interface for Interface {} + + impl Read for Interface { + fn read(&mut self, buf: &mut [u8]) -> std::io::Result; + } + + impl Write for Interface { + fn write(&mut self, buf: &[u8]) -> std::io::Result; + + fn flush(&mut self) -> std::io::Result<()>; + } + + impl NonBlock for Interface { + fn set_nonblocking(&mut self, enable: bool) -> crate::error::Result<()>; + } + } +} diff --git a/src/model/mod.rs b/src/model/mod.rs new file mode 100644 index 0000000..95155b3 --- /dev/null +++ b/src/model/mod.rs @@ -0,0 +1,27 @@ +use crate::{ + instrument::{info::get_info, Instrument}, + InstrumentError, Interface, +}; + +pub mod ki2600; +pub mod tti; +pub mod versatest; + +impl TryFrom> for Box { + type Error = InstrumentError; + + fn try_from(mut interface: Box) -> std::result::Result { + let info = get_info(interface.as_mut())?; + if tti::Instrument::is(&info) { + Ok(Box::new(tti::Instrument::new(interface))) + } else if ki2600::Instrument::is(&info) { + Ok(Box::new(ki2600::Instrument::new(interface))) + } else if versatest::Instrument::is(&info) { + Ok(Box::new(versatest::Instrument::new(interface))) + } else { + Err(InstrumentError::InstrumentError { + error: "unable to determine instrument type".to_string(), + }) + } + } +} diff --git a/src/model/resources/flashUtil.tsp b/src/model/resources/flashUtil.tsp new file mode 100644 index 0000000..a7cceab --- /dev/null +++ b/src/model/resources/flashUtil.tsp @@ -0,0 +1,185 @@ +-- This table encodes which firmware part numbers are expected for the various +-- model numbers reported by the module. The key is the model number reported +-- by the module. The value is the expected firmware part number of the image. +-- Note that unprogrammed modules may report their model number as the firmware +-- part number. +local gFwPartTable = +{ + ["Sparta"] = "066-2199", + ["VTSMU-48-2"] = "066-2199", + ["VTSMU-48-8"] = "066-2199", + ["VTSMU-48-16"] = "066-2199", + ["VTSMU-200-1"] = "066-2206", + ["VTSMU-200-1-LC"] = "066-2206", + ["VTSMU-200-2"] = "066-2206", + ["VTSMU-200-2-LC"] = "066-2206", + ["KingArthur"] = "066-2200", + ["VTPSU-50-2-ST"] = "066-2200", + ["900071100"] = "066-2205", -- This is the load test module + ["066-2199"] = "066-2199", + ["066-2200"] = "066-2200", + ["066-2205"] = "066-2205", + ["066-2206"] = "066-2206", +} + +-- This table encodes which firmware part numbers as reported by the firmware +-- image file are compatible with the expected firmware part number for the +-- module. That is to say, if the table above indicates firmware part number +-- 066-AAAA is the firmware to be loaded on the module, we check this table +-- using the firmware part number reported by the image as the key. If the +-- value on the right is 066-AAAA, we are good. +local gFwCompatibility = +{ + ["Sparta"] = "066-2199", + ["KingArthur"] = "066-2200", + ["066-2199"] = "066-2199", + ["066-2200"] = "066-2200", + ["066-2205"] = "066-2205", + ["066-2206"] = "066-2206", +} + +-- This table is used to indicate which modules use dual interfaces and +-- requires the firmware update function to program both interfaces. The +-- key is the model number. +local gSplitModules = +{ + ["VTSMU-48-16"] = true, + ["VTSMU-200-2"] = true, + ["VTSMU-200-2-LC"] = true, + ["900071100"] = true, +} + +checkbanks = function(lSlot, lDualBanks, lAction) + local lLowerBank = false + local lUpperBank = false + + if lDualBanks then + lLowerBank = lSlot.bank[1] + lUpperBank = lSlot.bank[2] + if lLowerBank and lLowerBank.flash then + lLowerBank = lLowerBank.flash + else + if lSlot.flash then + print("Warning: Bank[1] not detected but slot level interface detected.") + print("Warning: Assuming MRD image.") + lLowerBank = lSlot.flash + else + print("Warning: Bank[1] not detected. Lower bank will not be " .. lAction .. ".") + end + end + if lUpperBank then + lUpperBank = lUpperBank.flash + else + print("Warning: Bank[2] not detected. Upper bank will not be " .. lAction .. ".") + end + else + lLowerBank = lSlot.flash + end + + return lLowerBank, lUpperBank +end + +flashupdate = function(lSlot) + local lOffset = 0 + local lSectorNumber = 1 + local lSectorAddress + local lSectorData + local lPart + local lVersion + local lDualBanks + local lUpperBank = false + local lLowerBank = false + + lPart = gFwCompatibility[flash.part] + lVersion = flash.version + + if (flash.part == "MediumMrd") then + print("Loading MRD firmware onto model " .. lSlot.model) + elseif lPart ~= gFwPartTable[lSlot.model] then + print("Firmware image not appropriate for module. Module: " .. lSlot.model .. ", Image: " .. lPart) + print("Flash not updated.") + return + end + print("Updating module to version " .. lVersion) + + lDualBanks = gSplitModules[lSlot.model] + lLowerBank, lUpperBank = checkbanks(lSlot, lDualBanks, "programmed") + + errorqueue.clear() + lSectorAddress, lSectorData = flash.sector(lSectorNumber, true) + while lSectorAddress and (errorqueue.count == 0) do + print('Programming sector ' .. lSectorNumber) + if lLowerBank then + lLowerBank.sector.load(lSectorData) + lLowerBank.sector.program(lSectorAddress + lOffset) + end + if lUpperBank then + lUpperBank.sector.load(lSectorData) + lUpperBank.sector.program(lSectorAddress + lOffset) + end + lSectorNumber = lSectorNumber + 1 + lSectorAddress, lSectorData = flash.sector(lSectorNumber, true) + end + print("Done programming flash.") +end + +flashverify = function(lSlot, lCompress) + local lSectorNumber = 1 + local lSectorAddress + local lSectorData + local lRealData + local lDualBanks + local lUpperBank = false + local lLowerBank = false + + errorqueue.clear() + lDualBanks = gSplitModules[lSlot.model] + lLowerBank, lUpperBank = checkbanks(lSlot, lDualBanks, "checked") + lSectorAddress, lSectorData = flash.sector(lSectorNumber, lCompress) + while lSectorAddress and (errorqueue.count == 0) do + if lLowerBank then + lRealData = lLowerBank.read(lSectorAddress, 32768, lCompress) + if lRealData == lSectorData then + print('Sector ' .. lSectorNumber .. ' (lower) same') + else + print('Sector ' .. lSectorNumber .. ' (lower) different') + end + end + if lUpperBank then + lRealData = lUpperBank.read(lSectorAddress, 32768, lCompress) + if lRealData == lSectorData then + print('Sector ' .. lSectorNumber .. ' (upper) same') + else + print('Sector ' .. lSectorNumber .. ' (upper) different') + end + end + lSectorNumber = lSectorNumber + 1 + lSectorAddress, lSectorData = flash.sector(lSectorNumber, lCompress) + end +end + +flashencode = function (lAddress, lData) + local lLength = string.len(lData) + local lChunkLength + local lString + local lIndex + local lByteIndex + + lByteIndex = 1 + while lLength > 0 do + if lLength > 16 then + lChunkLength = 16 + else + lChunkLength = lLength + end + lString = string.format("S3%02X%08X", lChunkLength + 5, lAddress) + for lIndex = 1, lChunkLength do + lString = lString .. string.format("%02X", string.byte(lData, lByteIndex)) + lByteIndex = lByteIndex + 1 + end + lString = lString .. "--" + lLength = lLength - lChunkLength + lAddress = lAddress + lChunkLength + print(lString) + end +end diff --git a/src/model/tti.rs b/src/model/tti.rs new file mode 100644 index 0000000..0d1f8c4 --- /dev/null +++ b/src/model/tti.rs @@ -0,0 +1,970 @@ +use std::{ + io::{Read, Write}, + thread::sleep, + time::Duration, +}; + +use bytes::Buf; +use language::{CmdLanguage, Language}; + +use crate::{ + instrument::{self, info::InstrumentInfo, language, Info, Login, Script}, + interface::Interface, + interface::NonBlock, + Flash, InstrumentError, +}; + +pub struct Instrument { + info: Option, + interface: Box, +} + +impl Instrument { + #[must_use] + pub fn is(info: &InstrumentInfo) -> bool { + info.model.as_ref().map_or(false, is_tti) + } + + #[must_use] + pub const fn new(interface: Box) -> Self { + Self { + info: None, + interface, + } + } + + pub fn add_info(&mut self, info: InstrumentInfo) -> &Self { + self.info = Some(info); + self + } +} + +fn is_tti(model: impl AsRef) -> bool { + [ + "2450", "2470", "DMM7510", "2460", "2461", "2461-SYS", "DMM7512", "DMM6500", "DAQ6510", + ] + .contains(&model.as_ref()) +} + +//Implement device_interface::Interface since it is a subset of instrument::Instrument trait. +impl instrument::Instrument for Instrument {} + +impl Info for Instrument {} + +impl Language for Instrument { + fn get_language(&mut self) -> Result { + self.write_all(b"*LANG?\n")?; + let mut lang: Vec = vec![0; 16]; + let _read = self.read(&mut lang)?; + String::from_utf8_lossy(&lang).to_string().as_str().parse() + } + + fn change_language(&mut self, lang: CmdLanguage) -> Result<(), InstrumentError> { + self.write_all(format!("*LANG {lang}\n").as_bytes())?; + Ok(()) + } +} + +impl Login for Instrument { + fn check_login(&mut self) -> crate::error::Result { + self.write_all(b"*STB?\n")?; + + std::thread::sleep(Duration::from_millis(1000)); + + let mut resp: Vec = vec![0; 256]; + let _read = self.read(&mut resp)?; + + let resp = std::str::from_utf8(resp.as_slice()) + .unwrap_or("") + .trim_matches(char::from(0)) + .trim(); + + if resp.contains("FAILURE") { + Ok(instrument::State::Needed) + } else { + Ok(instrument::State::NotNeeded) + } + } + + fn login(&mut self, token: &[u8]) -> crate::error::Result<()> { + if instrument::State::NotNeeded == self.check_login()? { + return Ok(()); + } + + self.write_all(format!("login {}\n", String::from_utf8_lossy(token)).as_bytes())?; + + if instrument::State::Needed == self.check_login()? { + return Err(InstrumentError::LoginRejected); + } + + Ok(()) + } +} + +impl Script for Instrument {} + +impl Flash for Instrument { + fn flash_firmware(&mut self, image: &[u8], _: Option) -> crate::error::Result<()> { + const CHUNK_SIZE: usize = 1024; + //let len = image.len(); + //let mut written = 0usize; + let mut image = image.reader(); + + self.write_all(b"localnode.prompts=localnode.DISABLE\n")?; + self.write_all(b"if ki.upgrade ~= nil and ki.upgrade.noacklater ~= nil then ki.upgrade.noacklater() end\n")?; + self.write_all(b"prevflash\n")?; + + loop { + let mut buf = vec![0; CHUNK_SIZE]; + let b = image.read(&mut buf)?; + //written += b; + if b == 0 { + break; + } + let buf = &buf[..b]; + sleep(Duration::from_millis(1)); + self.write_all(buf)?; + } + + self.write_all(b"endflash\n")?; + Ok(()) + } +} + +impl Read for Instrument { + fn read(&mut self, buf: &mut [u8]) -> std::io::Result { + self.interface.read(buf) + } +} + +impl Write for Instrument { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + self.interface.write(buf) + } + + fn flush(&mut self) -> std::io::Result<()> { + self.interface.flush() + } +} + +impl NonBlock for Instrument { + fn set_nonblocking(&mut self, enable: bool) -> crate::error::Result<()> { + self.interface.set_nonblocking(enable) + } +} + +impl Drop for Instrument { + fn drop(&mut self) { + let _ = self.write_all(b"abort\n"); + } +} + +#[cfg(test)] +mod unit { + use std::{ + assert_matches::assert_matches, + io::{Read, Write}, + }; + + use bytes::Buf; + use mockall::{mock, Sequence}; + + use crate::{ + instrument::{self, info::Info, Language, Login, Script}, + interface::{self, NonBlock}, + test_util, Flash, InstrumentError, + }; + + use super::Instrument; + + #[test] + fn login_not_needed() { + let mut interface = MockInterface::new(); + + let mut seq = Sequence::new(); + + // A successful login attempt on a TTI instrument is as follows: + // 1. Instrument connects to interface + // 2. Instrument sends "*STB?\n" + // 3. Instrument reads from interface and receives status byte + // 4. Instrument returns `instrument::State::NotNeeded` + + interface + .expect_write() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf == b"*STB?\n") + .returning(|buf: &[u8]| Ok(buf.len())); + + interface + .expect_read() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf.len() >= 2) + .return_once(|buf: &mut [u8]| { + let msg = b"0\n"; + if buf.len() >= msg.len() { + let bytes = msg[..] + .reader() + .read(buf) + .expect("MockInterface should write to buffer"); + assert_eq!(bytes, msg.len()); + } + Ok(msg.len()) + }); + + interface + .expect_write() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf == b"*STB?\n") + .returning(|buf: &[u8]| Ok(buf.len())); + + interface + .expect_read() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf.len() >= 2) + .return_once(|buf: &mut [u8]| { + let msg = b"0\n"; + if buf.len() >= msg.len() { + let bytes = msg[..] + .reader() + .read(buf) + .expect("MockInterface should write to buffer"); + assert_eq!(bytes, msg.len()); + } + Ok(msg.len()) + }); + interface + .expect_write() + .times(..) + .withf(|buf: &[u8]| buf == b"abort\n") + .returning(|buf: &[u8]| Ok(buf.len())); + + let mut instrument: Instrument = Instrument::new(Box::new(interface)); + + assert_matches!(instrument.check_login(), Ok(instrument::State::NotNeeded)); + + assert!(instrument.login(b"secret_token").is_ok()); + } + + #[test] + #[allow(clippy::too_many_lines)] //Allow for now + fn login_success() { + let mut interface = MockInterface::new(); + + let mut seq = Sequence::new(); + + interface + .expect_write() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf == b"*STB?\n") + .returning(|buf: &[u8]| Ok(buf.len())); + + interface + .expect_read() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf.len() >= 8) + .return_once(|buf: &mut [u8]| { + let msg = b"FAILURE\n"; + if buf.len() >= msg.len() { + let bytes = msg[..] + .reader() + .read(buf) + .expect("MockInterface should write to buffer"); + assert_eq!(bytes, msg.len()); + } + Ok(msg.len()) + }); + + // login() { first check_login() } + interface + .expect_write() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf == b"*STB?\n") + .returning(|buf: &[u8]| Ok(buf.len())); + + interface + .expect_read() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf.len() >= 8) + .return_once(|buf: &mut [u8]| { + let msg = b"FAILURE\n"; + if buf.len() >= msg.len() { + let bytes = msg[..] + .reader() + .read(buf) + .expect("MockInterface should write to buffer"); + assert_eq!(bytes, msg.len()); + } + Ok(msg.len()) + }); + + // login() {write(b"login {token}")} + interface + .expect_write() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf == b"login secret_token\n") + .returning(|buf: &[u8]| Ok(buf.len())); + + // login() { second check_login() } + interface + .expect_write() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf == b"*STB?\n") + .returning(|buf: &[u8]| Ok(buf.len())); + + interface + .expect_read() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf.len() >= 2) + .return_once(|buf: &mut [u8]| { + let msg = b"0\n"; + if buf.len() >= msg.len() { + let bytes = msg[..] + .reader() + .read(buf) + .expect("MockInterface should write to buffer"); + assert_eq!(bytes, msg.len()); + } + Ok(msg.len()) + }); + + // check_login() + interface + .expect_write() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf == b"*STB?\n") + .returning(|buf: &[u8]| Ok(buf.len())); + + interface + .expect_read() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf.len() >= 2) + .return_once(|buf: &mut [u8]| { + let msg = b"0\n"; + if buf.len() >= msg.len() { + let bytes = msg[..] + .reader() + .read(buf) + .expect("MockInterface should write to buffer"); + assert_eq!(bytes, msg.len()); + } + Ok(msg.len()) + }); + + interface + .expect_write() + .times(..) + .withf(|buf: &[u8]| buf == b"abort\n") + .returning(|buf: &[u8]| Ok(buf.len())); + let mut instrument: Instrument = Instrument::new(Box::new(interface)); + + assert_matches!(instrument.check_login(), Ok(instrument::State::Needed)); + + assert_matches!(instrument.login(b"secret_token"), Ok(())); + + assert_matches!(instrument.check_login(), Ok(instrument::State::NotNeeded)); + } + + #[test] + #[allow(clippy::too_many_lines)] //Allow for now + fn login_failure() { + let mut interface = MockInterface::new(); + + let mut seq = Sequence::new(); + + // check_login() + interface + .expect_write() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf == b"*STB?\n") + .returning(|buf: &[u8]| Ok(buf.len())); + + interface + .expect_read() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf.len() >= 8) + .return_once(|buf: &mut [u8]| { + let msg = b"FAILURE\n"; + if buf.len() >= msg.len() { + let bytes = msg[..] + .reader() + .read(buf) + .expect("MockInterface should write to buffer"); + assert_eq!(bytes, msg.len()); + } + Ok(msg.len()) + }); + + // login() { first check_login() } + interface + .expect_write() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf == b"*STB?\n") + .returning(|buf: &[u8]| Ok(buf.len())); + + interface + .expect_read() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf.len() >= 8) + .return_once(|buf: &mut [u8]| { + let msg = b"FAILURE\n"; + if buf.len() >= msg.len() { + let bytes = msg[..] + .reader() + .read(buf) + .expect("MockInterface should write to buffer"); + assert_eq!(bytes, msg.len()); + } + Ok(msg.len()) + }); + + // login() {write(b"login {token}")} + interface + .expect_write() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf == b"login secret_token\n") + .returning(|buf: &[u8]| Ok(buf.len())); + + // login() { second check_login() } + interface + .expect_write() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf == b"*STB?\n") + .returning(|buf: &[u8]| Ok(buf.len())); + + interface + .expect_read() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf.len() >= 8) + .return_once(|buf: &mut [u8]| { + let msg = b"FAILURE\n"; + if buf.len() >= msg.len() { + let bytes = msg[..] + .reader() + .read(buf) + .expect("MockInterface should write to buffer"); + assert_eq!(bytes, msg.len()); + } + Ok(msg.len()) + }); + + // check_login() + interface + .expect_write() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf == b"*STB?\n") + .returning(|buf: &[u8]| Ok(buf.len())); + + interface + .expect_read() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf.len() >= 8) + .return_once(|buf: &mut [u8]| { + let msg = b"FAILURE\n"; + if buf.len() >= msg.len() { + let bytes = msg[..] + .reader() + .read(buf) + .expect("MockInterface should write to buffer"); + assert_eq!(bytes, msg.len()); + } + Ok(msg.len()) + }); + + interface + .expect_write() + .times(..) + .withf(|buf: &[u8]| buf == b"abort\n") + .returning(|buf: &[u8]| Ok(buf.len())); + let mut instrument: Instrument = Instrument::new(Box::new(interface)); + + assert_matches!(instrument.check_login(), Ok(instrument::State::Needed)); + + assert_matches!( + instrument.login(b"secret_token"), + Err(InstrumentError::LoginRejected) + ); + + assert_matches!(instrument.check_login(), Ok(instrument::State::Needed)); + } + + #[test] + fn info() { + let mut interface = MockInterface::new(); + + let mut seq = Sequence::new(); + + interface + .expect_set_nonblocking() + .times(..) + .returning(|_| Ok(())); + + // check_login() + interface + .expect_write() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf == b"*IDN?\n") + .returning(|buf: &[u8]| Ok(buf.len())); + + interface + .expect_read() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf.len() >= 50) + .return_once(|buf: &mut [u8]| { + let msg = b"KEITHLEY INSTRUMENTS,MODEL 2450,0123456789,1.2.3d\n"; + if buf.len() >= msg.len() { + let bytes = msg[..] + .reader() + .read(buf) + .expect("MockInterface should write to buffer"); + assert_eq!(bytes, msg.len()); + } + Ok(msg.len()) + }); + + interface + .expect_write() + .times(..) + .withf(|buf: &[u8]| buf == b"abort\n") + .returning(|buf: &[u8]| Ok(buf.len())); + let mut instrument: Instrument = Instrument::new(Box::new(interface)); + + let info = instrument + .info() + .expect("instrument can get instrument information from MockInterface"); + + let exp_vendor = "KEITHLEY INSTRUMENTS".to_string(); + let exp_model = "2450".to_string(); + let exp_serial = "0123456789".to_string(); + let exp_fw = "1.2.3d".to_string(); + + assert_eq!(info.vendor.unwrap(), exp_vendor); + assert_eq!(info.model.unwrap(), exp_model); + assert_eq!(info.serial_number.unwrap(), exp_serial); + assert_eq!(info.firmware_rev.unwrap(), exp_fw); + } + + #[test] + fn get_language_tsp() { + let mut interface = MockInterface::new(); + + let mut seq = Sequence::new(); + + interface + .expect_write() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf == b"*LANG?\n") + .returning(|buf: &[u8]| Ok(buf.len())); + + interface + .expect_read() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf.len() >= 4) + .return_once(|buf: &mut [u8]| { + let msg = b"TSP\n"; + if buf.len() >= msg.len() { + let bytes = msg[..] + .reader() + .read(buf) + .expect("MockInterface should write to buffer"); + assert_eq!(bytes, msg.len()); + } + Ok(msg.len()) + }); + + interface + .expect_write() + .times(..) + .withf(|buf: &[u8]| buf == b"abort\n") + .returning(|buf: &[u8]| Ok(buf.len())); + let mut instrument: Instrument = Instrument::new(Box::new(interface)); + + assert_eq!( + instrument.get_language().unwrap(), + instrument::CmdLanguage::Tsp + ); + } + + #[test] + fn get_language_scpi() { + let mut interface = MockInterface::new(); + + let mut seq = Sequence::new(); + interface + .expect_write() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf == b"*LANG?\n") + .returning(|buf: &[u8]| Ok(buf.len())); + + interface + .expect_read() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf.len() >= 5) + .return_once(|buf: &mut [u8]| { + let msg = b"SCPI\n"; + if buf.len() >= msg.len() { + let bytes = msg[..] + .reader() + .read(buf) + .expect("MockInterface should write to buffer"); + assert_eq!(bytes, msg.len()); + } + Ok(msg.len()) + }); + + interface + .expect_write() + .times(..) + .withf(|buf: &[u8]| buf == b"abort\n") + .returning(|buf: &[u8]| Ok(buf.len())); + let mut instrument: Instrument = Instrument::new(Box::new(interface)); + + assert_eq!( + instrument.get_language().unwrap(), + instrument::CmdLanguage::Scpi + ); + } + + #[test] + fn change_language() { + let mut interface = MockInterface::new(); + + let mut seq = Sequence::new(); + + interface + .expect_write() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf == b"*LANG TSP\n") + .returning(|buf: &[u8]| Ok(buf.len())); + + interface + .expect_write() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf == b"*LANG SCPI\n") + .returning(|buf: &[u8]| Ok(buf.len())); + interface + .expect_write() + .times(..) + .withf(|buf: &[u8]| buf == b"abort\n") + .returning(|buf: &[u8]| Ok(buf.len())); + + let mut instrument: Instrument = Instrument::new(Box::new(interface)); + + assert!(instrument + .change_language(instrument::CmdLanguage::Tsp) + .is_ok()); + + assert!(instrument + .change_language(instrument::CmdLanguage::Scpi) + .is_ok()); + } + + #[test] + fn write_script() { + let optional_writes: Vec> = vec![ + (*b"abort\n").into(), + (*b"_orig_prompts = localnode.prompts localnode.prompts = 0\n").into(), + (*b"localnode.prompts = _orig_prompts _orig_prompts = nil\n").into(), + ]; + let expected: Vec> = vec![ + (*b"test_script=nil\n").into(), + (*b"loadscript test_script\n").into(), + (*b"line1\n").into(), + (*b"line2\n").into(), + (*b"line3\n").into(), + (*b"\nendscript\n").into(), + ]; + let mut interface = MockInterface::new(); + + let mut seq = Sequence::new(); + + //Accept any number of flushes + interface.expect_flush().times(..).returning(|| Ok(())); + + for o in optional_writes { + interface + .expect_write() + .times(..) + .withf(move |buf: &[u8]| buf == o) + .returning(|buf: &[u8]| Ok(buf.len())); + } + + for e in expected { + interface + .expect_write() + .times(1) + .in_sequence(&mut seq) + .withf(move |buf: &[u8]| buf == e) + .returning(|buf: &[u8]| Ok(buf.len())); + } + let mut instrument: Instrument = Instrument::new(Box::new(interface)); + + instrument + .write_script(b"test_script", &b"line1\nline2\nline3"[..], false, false) + .expect("instrument should have written script to MockInterface"); + } + + #[test] + fn write_script_run() { + let optional_writes: Vec> = vec![ + (*b"abort\n").into(), + (*b"_orig_prompts = localnode.prompts localnode.prompts = 0\n").into(), + (*b"localnode.prompts = _orig_prompts _orig_prompts = nil\n").into(), + ]; + let expected: Vec> = vec![ + (*b"test_script=nil\n").into(), + (*b"loadscript test_script\n").into(), + (*b"line1\n").into(), + (*b"line2\n").into(), + (*b"line3\n").into(), + (*b"\nendscript\n").into(), + (*b"test_script.run()\n").into(), + ]; + let mut interface = MockInterface::new(); + + let mut seq = Sequence::new(); + + //Accept any number of flushes + interface.expect_flush().times(..).returning(|| Ok(())); + + for o in optional_writes { + interface + .expect_write() + .times(..) + .withf(move |buf: &[u8]| buf == o) + .returning(|buf: &[u8]| Ok(buf.len())); + } + + for e in expected { + interface + .expect_write() + .times(1) + .in_sequence(&mut seq) + .withf(move |buf: &[u8]| buf == e) + .returning(|buf: &[u8]| Ok(buf.len())); + } + let mut instrument: Instrument = Instrument::new(Box::new(interface)); + + instrument + .write_script(b"test_script", &b"line1\nline2\nline3"[..], false, true) + .expect("instrument should have written script to MockInterface"); + } + + #[test] + fn write_script_save() { + let optional_writes: Vec> = vec![ + (*b"abort\n").into(), + (*b"_orig_prompts = localnode.prompts localnode.prompts = 0\n").into(), + (*b"localnode.prompts = _orig_prompts _orig_prompts = nil\n").into(), + ]; + let expected: Vec> = vec![ + (*b"test_script=nil\n").into(), + (*b"loadscript test_script\n").into(), + (*b"line1\n").into(), + (*b"line2\n").into(), + (*b"line3\n").into(), + (*b"\nendscript\n").into(), + (*b"test_script.save()\n").into(), + ]; + let mut interface = MockInterface::new(); + + let mut seq = Sequence::new(); + + //Accept any number of flushes + interface.expect_flush().times(..).returning(|| Ok(())); + + for o in optional_writes { + interface + .expect_write() + .times(..) + .withf(move |buf: &[u8]| buf == o) + .returning(|buf: &[u8]| Ok(buf.len())); + } + + for e in expected { + interface + .expect_write() + .times(1) + .in_sequence(&mut seq) + .withf(move |buf: &[u8]| buf == e) + .returning(|buf: &[u8]| Ok(buf.len())); + } + + let mut instrument: Instrument = Instrument::new(Box::new(interface)); + + instrument + .write_script(b"test_script", &b"line1\nline2\nline3"[..], true, false) + .expect("instrument should have written script to MockInterface"); + } + + #[test] + fn write_script_save_run() { + let optional_writes: Vec> = vec![ + (*b"abort\n").into(), + (*b"_orig_prompts = localnode.prompts localnode.prompts = 0\n").into(), + (*b"localnode.prompts = _orig_prompts _orig_prompts = nil\n").into(), + ]; + let expected: Vec> = vec![ + (*b"test_script=nil\n").into(), + (*b"loadscript test_script\n").into(), + (*b"line1\n").into(), + (*b"line2\n").into(), + (*b"line3\n").into(), + (*b"\nendscript\n").into(), + (*b"test_script.save()\n").into(), + (*b"test_script.run()\n").into(), + ]; + let mut interface = MockInterface::new(); + + let mut seq = Sequence::new(); + + //Accept any number of flushes + interface.expect_flush().times(..).returning(|| Ok(())); + + for o in optional_writes { + interface + .expect_write() + .times(..) + .withf(move |buf: &[u8]| buf == o) + .returning(|buf: &[u8]| Ok(buf.len())); + } + + for e in expected { + interface + .expect_write() + .times(1) + .in_sequence(&mut seq) + .withf(move |buf: &[u8]| buf == e) + .returning(|buf: &[u8]| Ok(buf.len())); + } + + let mut instrument: Instrument = Instrument::new(Box::new(interface)); + + instrument + .write_script(b"test_script", &b"line1\nline2\nline3"[..], true, true) + .expect("instrument should have written script to MockInterface"); + } + + #[test] + fn flash_firmware() { + let mut interface = MockInterface::new(); + + let mut seq = Sequence::new(); + interface + .expect_write() + .times(..) + .withf(|buf: &[u8]| String::from_utf8_lossy(buf).contains("localnode.prompts")) + .returning(|buf: &[u8]| Ok(buf.len())); + + interface + .expect_write() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf == b"if ki.upgrade ~= nil and ki.upgrade.noacklater ~= nil then ki.upgrade.noacklater() end\n") + .returning(|buf: &[u8]| Ok(buf.len()) ); + + interface + .expect_write() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf == b"prevflash\n") + .returning(|buf: &[u8]| Ok(buf.len())); + + interface + .expect_write() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf == test_util::SIMPLE_FAKE_BINARY_CHUNK0) + .returning(|buf: &[u8]| Ok(buf.len())); + + interface + .expect_write() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf == test_util::SIMPLE_FAKE_BINARY_CHUNK1) + .returning(|buf: &[u8]| Ok(buf.len())); + + interface + .expect_write() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf == test_util::SIMPLE_FAKE_BINARY_CHUNK2) + .returning(|buf: &[u8]| Ok(buf.len())); + + interface + .expect_write() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf == test_util::SIMPLE_FAKE_BINARY_CHUNK3) + .returning(|buf: &[u8]| Ok(buf.len())); + + interface + .expect_write() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf == b"endflash\n") + .returning(|buf: &[u8]| Ok(buf.len())); + interface + .expect_write() + .times(..) + .withf(|buf: &[u8]| buf == b"abort\n") + .returning(|buf: &[u8]| Ok(buf.len())); + + let mut instrument: Instrument = Instrument::new(Box::new(interface)); + + instrument + .flash_firmware(test_util::SIMPLE_FAKE_BINARY_FW, Some(0)) + .expect("instrument should have written fw to MockInterface"); + } + + // Define a mock interface to be used in the tests above. + mock! { + Interface {} + + impl interface::Interface for Interface {} + + + impl Read for Interface { + fn read(&mut self, buf: &mut [u8]) -> std::io::Result; + } + + impl Write for Interface { + fn write(&mut self, buf: &[u8]) -> std::io::Result; + + fn flush(&mut self) -> std::io::Result<()>; + } + + impl NonBlock for Interface { + fn set_nonblocking(&mut self, enable: bool) -> crate::error::Result<()>; + } + } +} diff --git a/src/model/versatest.rs b/src/model/versatest.rs new file mode 100644 index 0000000..2979b13 --- /dev/null +++ b/src/model/versatest.rs @@ -0,0 +1,994 @@ +use std::{ + io::{BufRead, Read, Write}, + time::Duration, +}; + +use crate::{ + instrument::{self, info::InstrumentInfo, language, Info, Login, Script}, + interface::Interface, + interface::NonBlock, + Flash, InstrumentError, +}; +use bytes::Buf; +use language::Language; +use serde::{Deserialize, Serialize}; + +impl Instrument { + #[must_use] + pub fn is(info: &InstrumentInfo) -> bool { + info.model.as_ref().map_or(false, is_versatest) + } + + #[must_use] + pub const fn new(interface: Box) -> Self { + Self { + info: None, + interface, + } + } + + pub fn add_info(&mut self, info: InstrumentInfo) -> &Self { + self.info = Some(info); + self + } +} + +fn is_versatest(model: impl AsRef) -> bool { + ["VERSATEST-600", "TSPop"].contains(&model.as_ref()) +} + +pub struct Instrument { + info: Option, + interface: Box, +} + +//Implement device_interface::Interface since it is a subset of instrument::Instrument trait. +impl instrument::Instrument for Instrument {} + +impl Info for Instrument {} + +impl Language for Instrument {} + +impl Login for Instrument { + fn check_login(&mut self) -> crate::error::Result { + self.write_all(b"print('unlocked')\n")?; + + let mut resp: Vec = vec![0; 256]; + let _read = self.read(&mut resp)?; + + let resp = std::str::from_utf8(resp.as_slice()) + .unwrap_or("") + .trim_matches(char::from(0)) + .trim(); + + if resp.contains("FAILURE") { + Ok(instrument::State::Needed) + } else { + Ok(instrument::State::NotNeeded) + } + } + + fn login(&mut self, token: &[u8]) -> crate::error::Result<()> { + if instrument::State::NotNeeded == self.check_login()? { + return Ok(()); + } + + self.write_all(format!("password {}\n", String::from_utf8_lossy(token)).as_bytes())?; + + if instrument::State::Needed == self.check_login()? { + return Err(InstrumentError::LoginRejected); + } + + Ok(()) + } +} + +impl Script for Instrument {} + +impl Read for Instrument { + fn read(&mut self, buf: &mut [u8]) -> std::io::Result { + self.interface.read(buf) + } +} + +impl Write for Instrument { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + self.interface.write(buf) + } + + fn flush(&mut self) -> std::io::Result<()> { + self.interface.flush() + } +} + +/// The information necessary to flash an instrument. +#[derive(Serialize, Deserialize, Debug)] +struct FirmwareInfo { + /// For VersaTest only: `true` - the firmware is for a module; `false`: the firmware is for the mainframe + #[serde(rename = "IsModule")] + is_module: bool, + + /// For VersaTest only: The slot number of the module to update. + #[serde(rename = "Slot")] + slot: u8, +} +pub const VERSATEST_FLASH_UTIL_STR: &[u8] = include_bytes!("resources/flashUtil.tsp"); +impl Flash for Instrument { + fn flash_firmware( + &mut self, + image: &[u8], + firmware_info: Option, + ) -> crate::error::Result<()> { + let mut is_module = false; + let slot_number: u16 = firmware_info.unwrap_or(0); + if slot_number > 0 { + is_module = true; + } + + //TODO This is temporary: Only use while not defined in FW + if is_module { + self.write_script(b"FlashUtil", VERSATEST_FLASH_UTIL_STR, false, true)?; + } + //.update {"FileName": "C:/Users/esarver1/Downloads/trebuchet-mainframe-sd-225642.x", "IsModule": false, "Slot": 1} + //.update {"FileName": "C:/Users/esarver1/Downloads/kingarthur-module-225665.x", "IsModule": true, "Slot": 1} + + self.write_all(b"localnode.prompts=0\n")?; + let image = image.reader(); + self.write_all(b"flash\n")?; + + for line in image.lines() { + self.write_all(format!("{}\n", line?).as_bytes())?; + } + self.write_all(b"endflash\n")?; + + if is_module { + //TODO This is temporary: Only use while not defined in FW + self.write_all(b"FlashUtil()\n")?; + self.write_all(format!("flashupdate(slot[{slot_number}])\n").as_bytes())?; + + let flash_util_global_functions = [b"flashupdate", b"flashverify", b"flashencode"]; + + for func in flash_util_global_functions { + //wait before deleting functions + std::thread::sleep(Duration::from_millis(100)); + let _ = + self.write_all(format!("{} = nil\n", String::from_utf8_lossy(func)).as_bytes()); + } + + let script_name = "FlashUtil"; + self.write_all(format!("{script_name} = nil\n").as_bytes())?; + //TODO use this when the FW team has implemented it: + // self.write(format!("slot[{slot_number}].firmware.update()\n").as_bytes()); + } else { + //Update Mainframe + self.write_all(b"firmware.update()\n")?; + } + //self.write("localnode.prompts=1\n".to_string().as_bytes()); + + Ok(()) + } +} + +impl NonBlock for Instrument { + fn set_nonblocking(&mut self, enable: bool) -> crate::error::Result<()> { + self.interface.set_nonblocking(enable) + } +} + +impl Drop for Instrument { + fn drop(&mut self) { + let _ = self.interface.write_all(b"abort\n"); + } +} + +#[cfg(test)] +mod unit { + use std::{ + assert_matches::assert_matches, + io::{BufRead, Read, Write}, + }; + + use bytes::Buf; + use mockall::{mock, Sequence}; + + use crate::{ + instrument::{self, info::Info, Login, Script}, + interface::{self, NonBlock}, + test_util, Flash, InstrumentError, + }; + + use super::Instrument; + + #[test] + fn login_not_needed() { + let mut interface = MockInterface::new(); + + let mut seq = Sequence::new(); + + // A successful login attempt on a TTI instrument is as follows: + // 1. Instrument connects to interface + // 2. Instrument sends "*STB?\n" + // 3. Instrument reads from interface and receives status byte + // 4. Instrument returns `instrument::State::NotNeeded` + + interface + .expect_write() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf == b"print('unlocked')\n") + .returning(|buf: &[u8]| Ok(buf.len())); + + interface + .expect_read() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf.len() >= 2) + .return_once(|buf: &mut [u8]| { + let msg = b"unlocked\n"; + if buf.len() >= msg.len() { + let bytes = msg[..] + .reader() + .read(buf) + .expect("MockInterface should write to buffer"); + assert_eq!(bytes, msg.len()); + } + Ok(msg.len()) + }); + + interface + .expect_write() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf == b"print('unlocked')\n") + .returning(|buf: &[u8]| Ok(buf.len())); + + interface + .expect_read() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf.len() >= 2) + .return_once(|buf: &mut [u8]| { + let msg = b"unlocked\n"; + if buf.len() >= msg.len() { + let bytes = msg[..] + .reader() + .read(buf) + .expect("MockInterface should write to buffer"); + assert_eq!(bytes, msg.len()); + } + Ok(msg.len()) + }); + interface + .expect_write() + .times(..) + .withf(|buf: &[u8]| buf == b"abort\n") + .returning(|buf: &[u8]| Ok(buf.len())); + let mut instrument: Instrument = Instrument::new(Box::new(interface)); + + assert_matches!(instrument.check_login(), Ok(instrument::State::NotNeeded)); + + assert!(instrument.login(b"secret_token").is_ok()); + } + + #[test] + #[allow(clippy::too_many_lines)] //Allow for now. + fn login_success() { + let mut interface = MockInterface::new(); + + let mut seq = Sequence::new(); + + interface + .expect_write() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf == b"print('unlocked')\n") + .returning(|buf: &[u8]| Ok(buf.len())); + + interface + .expect_read() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf.len() >= 8) + .return_once(|buf: &mut [u8]| { + let msg = b"FAILURE\n"; + if buf.len() >= msg.len() { + let bytes = msg[..] + .reader() + .read(buf) + .expect("MockInstrument should write to buffer"); + assert_eq!(bytes, msg.len()); + } + Ok(msg.len()) + }); + + // login() { first check_login() } + interface + .expect_write() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf == b"print('unlocked')\n") + .returning(|buf: &[u8]| Ok(buf.len())); + + interface + .expect_read() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf.len() >= 8) + .return_once(|buf: &mut [u8]| { + let msg = b"FAILURE\n"; + if buf.len() >= msg.len() { + let bytes = msg[..] + .reader() + .read(buf) + .expect("MockInstrument should write to buffer"); + assert_eq!(bytes, msg.len()); + } + Ok(msg.len()) + }); + + // login() {write(b"login {token}")} + interface + .expect_write() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf == b"password secret_token\n") + .returning(|buf: &[u8]| Ok(buf.len())); + + // login() { second check_login() } + interface + .expect_write() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf == b"print('unlocked')\n") + .returning(|buf: &[u8]| Ok(buf.len())); + + interface + .expect_read() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf.len() >= 8) + .return_once(|buf: &mut [u8]| { + let msg = b"unlocked\n"; + if buf.len() >= msg.len() { + let bytes = msg[..] + .reader() + .read(buf) + .expect("MockInstrument should write to buffer"); + assert_eq!(bytes, msg.len()); + } + Ok(msg.len()) + }); + + // check_login() + interface + .expect_write() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf == b"print('unlocked')\n") + .returning(|buf: &[u8]| Ok(buf.len())); + + interface + .expect_read() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf.len() >= 8) + .return_once(|buf: &mut [u8]| { + let msg = b"unlocked\n"; + if buf.len() >= msg.len() { + let bytes = msg[..] + .reader() + .read(buf) + .expect("MockInstrument should write to buffer"); + assert_eq!(bytes, msg.len()); + } + Ok(msg.len()) + }); + interface + .expect_write() + .times(..) + .withf(|buf: &[u8]| buf == b"abort\n") + .returning(|buf: &[u8]| Ok(buf.len())); + + let mut instrument: Instrument = Instrument::new(Box::new(interface)); + + assert_matches!(instrument.check_login(), Ok(instrument::State::Needed)); + + assert_matches!(instrument.login(b"secret_token"), Ok(())); + + assert_matches!(instrument.check_login(), Ok(instrument::State::NotNeeded)); + } + + #[test] + #[allow(clippy::too_many_lines)] //Allow for now + fn login_failure() { + let mut interface = MockInterface::new(); + + let mut seq = Sequence::new(); + + interface + .expect_write() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf == b"print('unlocked')\n") + .returning(|buf: &[u8]| Ok(buf.len())); + + interface + .expect_read() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf.len() >= 8) + .return_once(|buf: &mut [u8]| { + let msg = b"FAILURE\n"; + if buf.len() >= msg.len() { + let bytes = msg[..] + .reader() + .read(buf) + .expect("MockInstrument should write to buffer"); + assert_eq!(bytes, msg.len()); + } + Ok(msg.len()) + }); + + // login() { first check_login() } + interface + .expect_write() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf == b"print('unlocked')\n") + .returning(|buf: &[u8]| Ok(buf.len())); + + interface + .expect_read() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf.len() >= 8) + .return_once(|buf: &mut [u8]| { + let msg = b"FAILURE\n"; + if buf.len() >= msg.len() { + let bytes = msg[..] + .reader() + .read(buf) + .expect("MockInstrument should write to buffer"); + assert_eq!(bytes, msg.len()); + } + Ok(msg.len()) + }); + + // login() {write(b"login {token}")} + interface + .expect_write() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf == b"password secret_token\n") + .returning(|buf: &[u8]| Ok(buf.len())); + + // login() { second check_login() } + interface + .expect_write() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf == b"print('unlocked')\n") + .returning(|buf: &[u8]| Ok(buf.len())); + + interface + .expect_read() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf.len() >= 8) + .return_once(|buf: &mut [u8]| { + let msg = b"FAILURE\n"; + if buf.len() >= msg.len() { + let bytes = msg[..] + .reader() + .read(buf) + .expect("MockInstrument should write to buffer"); + assert_eq!(bytes, msg.len()); + } + Ok(msg.len()) + }); + + // check_login() + interface + .expect_write() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf == b"print('unlocked')\n") + .returning(|buf: &[u8]| Ok(buf.len())); + + interface + .expect_read() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf.len() >= 8) + .return_once(|buf: &mut [u8]| { + let msg = b"FAILURE\n"; + if buf.len() >= msg.len() { + let bytes = msg[..] + .reader() + .read(buf) + .expect("MockInstrument should write to buffer"); + assert_eq!(bytes, msg.len()); + } + Ok(msg.len()) + }); + interface + .expect_write() + .times(..) + .withf(|buf: &[u8]| buf == b"abort\n") + .returning(|buf: &[u8]| Ok(buf.len())); + + let mut instrument: Instrument = Instrument::new(Box::new(interface)); + + assert_matches!(instrument.check_login(), Ok(instrument::State::Needed)); + + assert_matches!( + instrument.login(b"secret_token"), + Err(InstrumentError::LoginRejected) + ); + + assert_matches!(instrument.check_login(), Ok(instrument::State::Needed)); + } + + #[test] + fn info() { + let mut interface = MockInterface::new(); + + let mut seq = Sequence::new(); + + interface + .expect_write() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf == b"*IDN?\n") + .returning(|buf: &[u8]| Ok(buf.len())); + + interface + .expect_read() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf.len() >= 50) + .return_once(|buf: &mut [u8]| { + let msg = b"KEITHLEY INSTRUMENTS,MODEL 2450,0123456789,1.2.3d\n"; + if buf.len() >= msg.len() { + let bytes = msg[..] + .reader() + .read(buf) + .expect("MockInterface should write to buffer"); + assert_eq!(bytes, msg.len()); + } + Ok(msg.len()) + }); + interface + .expect_write() + .times(..) + .withf(|buf: &[u8]| buf == b"abort\n") + .returning(|buf: &[u8]| Ok(buf.len())); + + let mut instrument: Instrument = Instrument::new(Box::new(interface)); + + let info = instrument + .info() + .expect("instrument can get instrument information from MockInterface"); + + let exp_vendor = "KEITHLEY INSTRUMENTS".to_string(); + let exp_model = "2450".to_string(); + let exp_serial = "0123456789".to_string(); + let exp_fw = "1.2.3d".to_string(); + + assert_eq!(info.vendor.unwrap(), exp_vendor); + assert_eq!(info.model.unwrap(), exp_model); + assert_eq!(info.serial_number.unwrap(), exp_serial); + assert_eq!(info.firmware_rev.unwrap(), exp_fw); + } + + #[test] + fn write_script() { + let mut interface = MockInterface::new(); + + let mut seq = Sequence::new(); + + interface + .expect_write() + .times(..) + .withf(|buf: &[u8]| buf == b"_orig_prompts = localnode.prompts localnode.prompts = 0\n") + .returning(|buf: &[u8]| Ok(buf.len())); + + interface + .expect_write() + .times(..) + .withf(|buf: &[u8]| buf == b"localnode.prompts = _orig_prompts _orig_prompts = nil\n") + .returning(|buf: &[u8]| Ok(buf.len())); + + //Accept any number of flushes + interface.expect_flush().times(..).returning(|| Ok(())); + + interface + .expect_write() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf == b"test_script=nil\n") + .returning(|buf: &[u8]| Ok(buf.len())); + + interface + .expect_write() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf == b"loadscript test_script\n") + .returning(|buf: &[u8]| Ok(buf.len())); + + interface + .expect_write() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf == b"line1\n") + .returning(|buf: &[u8]| Ok(buf.len())); + + interface + .expect_write() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf == b"line2\n") + .returning(|buf: &[u8]| Ok(buf.len())); + + interface + .expect_write() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf == b"line3\n") + .returning(|buf: &[u8]| Ok(buf.len())); + + interface + .expect_write() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf == b"\nendscript\n") + .returning(|buf: &[u8]| Ok(buf.len())); + interface + .expect_write() + .times(..) + .withf(|buf: &[u8]| buf == b"abort\n") + .returning(|buf: &[u8]| Ok(buf.len())); + + let mut instrument: Instrument = Instrument::new(Box::new(interface)); + + instrument + .write_script(b"test_script", &b"line1\nline2\nline3"[..], false, false) + .expect("instrument should have written script to MockInterface"); + } + + #[test] + fn write_script_run() { + let mut interface = MockInterface::new(); + + let mut seq = Sequence::new(); + + interface + .expect_write() + .times(..) + .withf(|buf: &[u8]| buf == b"_orig_prompts = localnode.prompts localnode.prompts = 0\n") + .returning(|buf: &[u8]| Ok(buf.len())); + + interface + .expect_write() + .times(..) + .withf(|buf: &[u8]| buf == b"localnode.prompts = _orig_prompts _orig_prompts = nil\n") + .returning(|buf: &[u8]| Ok(buf.len())); + + //Accept any number of flushes + interface.expect_flush().times(..).returning(|| Ok(())); + + interface + .expect_write() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf == b"test_script=nil\n") + .returning(|buf: &[u8]| Ok(buf.len())); + + interface + .expect_write() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf == b"loadscript test_script\n") + .returning(|buf: &[u8]| Ok(buf.len())); + + interface + .expect_write() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf == b"line1\n") + .returning(|buf: &[u8]| Ok(buf.len())); + + interface + .expect_write() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf == b"line2\n") + .returning(|buf: &[u8]| Ok(buf.len())); + + interface + .expect_write() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf == b"line3\n") + .returning(|buf: &[u8]| Ok(buf.len())); + + interface + .expect_write() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf == b"\nendscript\n") + .returning(|buf: &[u8]| Ok(buf.len())); + + interface + .expect_write() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf == b"test_script.run()\n") + .returning(|buf: &[u8]| Ok(buf.len())); + interface + .expect_write() + .times(..) + .withf(|buf: &[u8]| buf == b"abort\n") + .returning(|buf: &[u8]| Ok(buf.len())); + + let mut instrument: Instrument = Instrument::new(Box::new(interface)); + + instrument + .write_script(b"test_script", &b"line1\nline2\nline3"[..], false, true) + .expect("instrument should have written script to MockInterface"); + } + + #[test] + fn write_script_save() { + let mut interface = MockInterface::new(); + + let mut seq = Sequence::new(); + + interface + .expect_write() + .times(..) + .withf(|buf: &[u8]| buf == b"_orig_prompts = localnode.prompts localnode.prompts = 0\n") + .returning(|buf: &[u8]| Ok(buf.len())); + + interface + .expect_write() + .times(..) + .withf(|buf: &[u8]| buf == b"localnode.prompts = _orig_prompts _orig_prompts = nil\n") + .returning(|buf: &[u8]| Ok(buf.len())); + + //Accept any number of flushes + interface.expect_flush().times(..).returning(|| Ok(())); + + interface + .expect_write() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf == b"test_script=nil\n") + .returning(|buf: &[u8]| Ok(buf.len())); + + interface + .expect_write() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf == b"loadscript test_script\n") + .returning(|buf: &[u8]| Ok(buf.len())); + + interface + .expect_write() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf == b"line1\n") + .returning(|buf: &[u8]| Ok(buf.len())); + + interface + .expect_write() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf == b"line2\n") + .returning(|buf: &[u8]| Ok(buf.len())); + + interface + .expect_write() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf == b"line3\n") + .returning(|buf: &[u8]| Ok(buf.len())); + + interface + .expect_write() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf == b"\nendscript\n") + .returning(|buf: &[u8]| Ok(buf.len())); + + interface + .expect_write() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf == b"test_script.save()\n") + .returning(|buf: &[u8]| Ok(buf.len())); + interface + .expect_write() + .times(..) + .withf(|buf: &[u8]| buf == b"abort\n") + .returning(|buf: &[u8]| Ok(buf.len())); + + let mut instrument: Instrument = Instrument::new(Box::new(interface)); + + instrument + .write_script(b"test_script", &b"line1\nline2\nline3"[..], true, false) + .expect("instrument should have written script to MockInterface"); + } + + #[test] + fn write_script_save_run() { + let mut interface = MockInterface::new(); + + let mut seq = Sequence::new(); + + interface + .expect_write() + .times(..) + .withf(|buf: &[u8]| buf == b"_orig_prompts = localnode.prompts localnode.prompts = 0\n") + .returning(|buf: &[u8]| Ok(buf.len())); + + interface + .expect_write() + .times(..) + .withf(|buf: &[u8]| buf == b"localnode.prompts = _orig_prompts _orig_prompts = nil\n") + .returning(|buf: &[u8]| Ok(buf.len())); + + //Accept any number of flushes + interface.expect_flush().times(..).returning(|| Ok(())); + + interface + .expect_write() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf == b"test_script=nil\n") + .returning(|buf: &[u8]| Ok(buf.len())); + + interface + .expect_write() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf == b"loadscript test_script\n") + .returning(|buf: &[u8]| Ok(buf.len())); + + interface + .expect_write() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf == b"line1\n") + .returning(|buf: &[u8]| Ok(buf.len())); + + interface + .expect_write() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf == b"line2\n") + .returning(|buf: &[u8]| Ok(buf.len())); + + interface + .expect_write() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf == b"line3\n") + .returning(|buf: &[u8]| Ok(buf.len())); + + interface + .expect_write() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf == b"\nendscript\n") + .returning(|buf: &[u8]| Ok(buf.len())); + + interface + .expect_write() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf == b"test_script.save()\n") + .returning(|buf: &[u8]| Ok(buf.len())); + + interface + .expect_write() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf == b"test_script.run()\n") + .returning(|buf: &[u8]| Ok(buf.len())); + + interface + .expect_write() + .times(..) + .withf(|buf: &[u8]| buf == b"abort\n") + .returning(|buf: &[u8]| Ok(buf.len())); + let mut instrument: Instrument = Instrument::new(Box::new(interface)); + + instrument + .write_script(b"test_script", &b"line1\nline2\nline3"[..], true, true) + .expect("instrument should have written script to MockInterface"); + } + + #[test] + fn flash_firmware() { + let mut interface = MockInterface::new(); + + let mut seq = Sequence::new(); + + interface + .expect_write() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf == b"localnode.prompts=0\n") + .returning(|buf: &[u8]| Ok(buf.len())); + + interface + .expect_write() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf == b"flash\n") + .returning(|buf: &[u8]| Ok(buf.len())); + + for line in test_util::SIMPLE_FAKE_TEXTUAL_FW.reader().lines() { + interface + .expect_write() + .times(1) + .in_sequence(&mut seq) + .withf(move |buf: &[u8]| { + buf == format!( + "{}\n", + line.as_ref() + .expect("textual test firmware should return all Ok for lines()") + ) + .as_bytes() + }) + .returning(|buf: &[u8]| Ok(buf.len())); + } + + interface + .expect_write() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf == b"endflash\n") + .returning(|buf: &[u8]| Ok(buf.len())); + + interface + .expect_write() + .times(1) + .in_sequence(&mut seq) + .withf(|buf: &[u8]| buf == b"firmware.update()\n") + .returning(|buf: &[u8]| Ok(buf.len())); + interface + .expect_write() + .times(..) + .withf(|buf: &[u8]| buf == b"abort\n") + .returning(|buf: &[u8]| Ok(buf.len())); + + let mut instrument: Instrument = Instrument::new(Box::new(interface)); + + instrument + .flash_firmware(test_util::SIMPLE_FAKE_TEXTUAL_FW, Some(0)) + .expect("instrument should have written fw to MockInterface"); + } + + // Define a mock interface to be used in the tests above. + mock! { + Interface {} + + impl interface::Interface for Interface {} + + + impl Read for Interface { + fn read(&mut self, buf: &mut [u8]) -> std::io::Result; + } + + impl Write for Interface { + fn write(&mut self, buf: &[u8]) -> std::io::Result; + + fn flush(&mut self) -> std::io::Result<()>; + } + impl NonBlock for Interface { + fn set_nonblocking(&mut self, enable: bool) -> crate::error::Result<()>; + } + } +} diff --git a/src/test_util/mod.rs b/src/test_util/mod.rs new file mode 100644 index 0000000..7af739d --- /dev/null +++ b/src/test_util/mod.rs @@ -0,0 +1,6 @@ +pub const SIMPLE_FAKE_BINARY_FW: &[u8] = include_bytes!("./simple_fake_binary_fw.test"); +pub const SIMPLE_FAKE_BINARY_CHUNK0: &[u8] = include_bytes!("./simple_fake_binary_fw.chunk0"); +pub const SIMPLE_FAKE_BINARY_CHUNK1: &[u8] = include_bytes!("./simple_fake_binary_fw.chunk1"); +pub const SIMPLE_FAKE_BINARY_CHUNK2: &[u8] = include_bytes!("./simple_fake_binary_fw.chunk2"); +pub const SIMPLE_FAKE_BINARY_CHUNK3: &[u8] = include_bytes!("./simple_fake_binary_fw.chunk3"); +pub const SIMPLE_FAKE_TEXTUAL_FW: &[u8] = include_bytes!("./simple_fake_textual_fw.test"); diff --git a/src/test_util/simple_fake_binary_fw.chunk0 b/src/test_util/simple_fake_binary_fw.chunk0 new file mode 100644 index 0000000..7522b6b Binary files /dev/null and b/src/test_util/simple_fake_binary_fw.chunk0 differ diff --git a/src/test_util/simple_fake_binary_fw.chunk1 b/src/test_util/simple_fake_binary_fw.chunk1 new file mode 100644 index 0000000..90600f9 Binary files /dev/null and b/src/test_util/simple_fake_binary_fw.chunk1 differ diff --git a/src/test_util/simple_fake_binary_fw.chunk2 b/src/test_util/simple_fake_binary_fw.chunk2 new file mode 100644 index 0000000..e677bf4 Binary files /dev/null and b/src/test_util/simple_fake_binary_fw.chunk2 differ diff --git a/src/test_util/simple_fake_binary_fw.chunk3 b/src/test_util/simple_fake_binary_fw.chunk3 new file mode 100644 index 0000000..b41eac0 Binary files /dev/null and b/src/test_util/simple_fake_binary_fw.chunk3 differ diff --git a/src/test_util/simple_fake_binary_fw.test b/src/test_util/simple_fake_binary_fw.test new file mode 100644 index 0000000..e911c8e Binary files /dev/null and b/src/test_util/simple_fake_binary_fw.test differ diff --git a/src/test_util/simple_fake_textual_fw.test b/src/test_util/simple_fake_textual_fw.test new file mode 100644 index 0000000..980d988 --- /dev/null +++ b/src/test_util/simple_fake_textual_fw.test @@ -0,0 +1,11 @@ + !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~ + !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~ + !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~ + !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~ + !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~ + !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~ + !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~ + !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~ + !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~ + !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~ + !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_ \ No newline at end of file