From e35f5b243b9ea2d5d046267fba60b6251abf7c38 Mon Sep 17 00:00:00 2001
From: hinto-janaiyo <hinto.janaiyo@protonmail.com>
Date: Sat, 17 Dec 2022 17:17:26 -0500
Subject: [PATCH] Add #[test]s, fix [State::merge()], fix [quit_twice]

---
 Cargo.lock       |  52 +++---
 src/constants.rs |  21 +++
 src/disk.rs      | 421 ++++++++++++++++++++++++++++++++++++++---------
 src/helper.rs    | 176 ++++++++++++++++++++
 src/main.rs      |  40 ++++-
 src/node.rs      |  33 +++-
 6 files changed, 627 insertions(+), 116 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock
index 9495c53..c2eaad3 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2095,9 +2095,9 @@ dependencies = [
 
 [[package]]
 name = "itoa"
-version = "1.0.4"
+version = "1.0.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4217ad341ebadf8d8e724e264f13e593e0648f5b3e94b3896a5df283be015ecc"
+checksum = "fad582f4b9e86b6caa621cabeb0963332d92eea04729ab12892c2533951e6440"
 
 [[package]]
 name = "jni"
@@ -2787,9 +2787,9 @@ dependencies = [
 
 [[package]]
 name = "paste"
-version = "1.0.10"
+version = "1.0.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cf1c2c742266c2f1041c914ba65355a83ae8747b05f208319784083583494b4b"
+checksum = "d01a5bd0424d00070b0098dd17ebca6f961a959dead1dbcbbbc1d1cd8d3deeba"
 
 [[package]]
 name = "pathdiff"
@@ -3009,9 +3009,9 @@ checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5"
 
 [[package]]
 name = "proc-macro2"
-version = "1.0.47"
+version = "1.0.48"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5ea3d908b0e36316caf9e9e2c4625cdde190a7e6f440d794667ed17a1855e725"
+checksum = "e9d89e5dba24725ae5678020bf8f1357a9aa7ff10736b551adbcd3f8d17d766f"
 dependencies = [
  "unicode-ident",
 ]
@@ -3024,9 +3024,9 @@ checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0"
 
 [[package]]
 name = "quote"
-version = "1.0.21"
+version = "1.0.22"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179"
+checksum = "556d0f47a940e895261e77dc200d5eadfc6ef644c179c6f5edfc105e3a2292c8"
 dependencies = [
  "proc-macro2",
 ]
@@ -3291,15 +3291,15 @@ dependencies = [
 
 [[package]]
 name = "rustversion"
-version = "1.0.9"
+version = "1.0.10"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "97477e48b4cf8603ad5f7aaf897467cf42ab4218a38ef76fb14c2d6773a6d6a8"
+checksum = "4378ea89513870b6e2303ec50618e97da0fa43cdd9ded83ad3b6bad2693c08c6"
 
 [[package]]
 name = "ryu"
-version = "1.0.11"
+version = "1.0.12"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09"
+checksum = "7b4b9743ed687d4b4bcedf9ff5eaa7398495ae14e61cba0a295704edbc7decde"
 
 [[package]]
 name = "safe_arch"
@@ -3405,9 +3405,9 @@ dependencies = [
 
 [[package]]
 name = "semver"
-version = "1.0.14"
+version = "1.0.15"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e25dfac463d778e353db5be2449d1cce89bd6fd23c9f1ea21310ce6e5a1b29c4"
+checksum = "3bfa246f936730408c0abee392cc1a50b118ece708c7f630516defd64480c7d8"
 
 [[package]]
 name = "serde"
@@ -3431,18 +3431,18 @@ dependencies = [
 
 [[package]]
 name = "serde_ignored"
-version = "0.1.5"
+version = "0.1.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "82b3da7eedd967647a866f67829d1c79d184d7c4521126e9cc2c46a9585c6d21"
+checksum = "51212eb6171778353d78ef5860fdffe29ac17f03a69375d2dc14fbb5754d54a4"
 dependencies = [
  "serde",
 ]
 
 [[package]]
 name = "serde_json"
-version = "1.0.89"
+version = "1.0.90"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "020ff22c755c2ed3f8cf162dbb41a7268d934702f3ed3631656ea597e08fc3db"
+checksum = "8778cc0b528968fe72abec38b5db5a20a70d148116cd9325d2bc5f5180ca3faf"
 dependencies = [
  "itoa",
  "ryu",
@@ -3800,9 +3800,9 @@ dependencies = [
 
 [[package]]
 name = "syn"
-version = "1.0.105"
+version = "1.0.106"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "60b9b43d45702de4c839cb9b51d9f529c5dd26a4aff255b42b1ebc03e88ee908"
+checksum = "09ee3a69cd2c7e06684677e5629b3878b253af05e4714964204279c6bc02cf0b"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -3904,18 +3904,18 @@ dependencies = [
 
 [[package]]
 name = "thiserror"
-version = "1.0.37"
+version = "1.0.38"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "10deb33631e3c9018b9baf9dcbbc4f737320d2b576bac10f6aefa048fa407e3e"
+checksum = "6a9cd18aa97d5c45c6603caea1da6628790b37f7a34b6ca89522331c5180fed0"
 dependencies = [
  "thiserror-impl",
 ]
 
 [[package]]
 name = "thiserror-impl"
-version = "1.0.37"
+version = "1.0.38"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "982d17546b47146b28f7c22e3d08465f6b8903d0ea13c1660d9d84a6e7adcdbb"
+checksum = "1fb327af4685e4d03fa8cbcf1716380da910eeb2bb8be417e7f9fd3fb164f36f"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -4730,9 +4730,9 @@ checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992"
 
 [[package]]
 name = "unicode-ident"
-version = "1.0.5"
+version = "1.0.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6ceab39d59e4c9499d4e5a8ee0e2735b891bb7308ac83dfb4e80cad195c9f6f3"
+checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc"
 
 [[package]]
 name = "unicode-normalization"
diff --git a/src/constants.rs b/src/constants.rs
index ec500cc..a4e0404 100644
--- a/src/constants.rs
+++ b/src/constants.rs
@@ -293,3 +293,24 @@ pub const ARG_COPYRIGHT: &str =
 r#"Gupax is licensed under GPLv3.
 For more information, see link below:
 <https://github.com/hinto-janaiyo/gupax>"#;
+
+//---------------------------------------------------------------------------------------------------- TESTS
+#[cfg(test)]
+mod test {
+	#[test]
+	fn gupax_version_is_semver() {
+		assert_eq!(crate::GUPAX_VERSION.len(), 6);
+	}
+
+	#[test]
+	fn app_ratio_is_4_by_3() {
+		assert_eq!(format!("{:.3}", crate::APP_MIN_WIDTH/crate::APP_MIN_HEIGHT), "1.333");
+		assert_eq!(format!("{:.3}", crate::APP_MAX_WIDTH/crate::APP_MAX_HEIGHT), "1.333");
+		assert_eq!(format!("{:.3}", crate::APP_DEFAULT_WIDTH/crate::APP_DEFAULT_HEIGHT), "1.333");
+	}
+
+	#[test]
+	fn git_commit_is_41_chars() {
+		assert_eq!(crate::COMMIT.len(), 41);
+	}
+}
diff --git a/src/disk.rs b/src/disk.rs
index 3c7e26d..a115128 100644
--- a/src/disk.rs
+++ b/src/disk.rs
@@ -151,74 +151,10 @@ impl State {
 		let max_threads = num_cpus::get();
 		let current_threads = if max_threads == 1 { 1 } else { max_threads / 2 };
 		Self {
-			gupax: Gupax {
-				simple: true,
-				auto_update: true,
-				auto_p2pool: false,
-				auto_xmrig: false,
-				ask_before_quit: true,
-				save_before_quit: true,
-				#[cfg(not(target_os = "macos"))]
-				update_via_tor: true,
-				#[cfg(target_os = "macos")] // Arti library has issues on macOS
-				update_via_tor: false,
-				p2pool_path: DEFAULT_P2POOL_PATH.to_string(),
-				xmrig_path: DEFAULT_XMRIG_PATH.to_string(),
-				absolute_p2pool_path: into_absolute_path(DEFAULT_P2POOL_PATH.to_string()).unwrap(),
-				absolute_xmrig_path: into_absolute_path(DEFAULT_XMRIG_PATH.to_string()).unwrap(),
-				selected_width: APP_DEFAULT_WIDTH as u16,
-				selected_height: APP_DEFAULT_HEIGHT as u16,
-				ratio: Ratio::Width,
-				tab: Tab::About,
-			},
-			p2pool: P2pool {
-				simple: true,
-				mini: true,
-				auto_ping: true,
-				auto_select: true,
-				out_peers: 10,
-				in_peers: 10,
-				log_level: 3,
-				node: crate::NodeEnum::C3pool,
-				arguments: String::new(),
-				address: String::with_capacity(96),
-				name: "Local Monero Node".to_string(),
-				ip: "localhost".to_string(),
-				rpc: "18081".to_string(),
-				zmq: "18083".to_string(),
-				selected_index: 0,
-				selected_name: "Local Monero Node".to_string(),
-				selected_ip: "localhost".to_string(),
-				selected_rpc: "18081".to_string(),
-				selected_zmq: "18083".to_string(),
-			},
-			xmrig: Xmrig {
-				simple: true,
-				pause: 0,
-				simple_rig: String::with_capacity(30),
-				arguments: String::with_capacity(300),
-				address: String::with_capacity(96),
-				name: "Local P2Pool".to_string(),
-				rig: GUPAX_VERSION_UNDERSCORE.to_string(),
-				ip: "localhost".to_string(),
-				port: "3333".to_string(),
-				selected_index: 0,
-				selected_name: "Local P2Pool".to_string(),
-				selected_ip: "localhost".to_string(),
-				selected_rig: GUPAX_VERSION_UNDERSCORE.to_string(),
-				selected_port: "3333".to_string(),
-				api_ip: "localhost".to_string(),
-				api_port: "18088".to_string(),
-				tls: false,
-				keepalive: false,
-				current_threads,
-				max_threads,
-			},
-			version: Arc::new(Mutex::new(Version {
-				gupax: GUPAX_VERSION.to_string(),
-				p2pool: P2POOL_VERSION.to_string(),
-				xmrig: XMRIG_VERSION.to_string(),
-			})),
+			gupax: Gupax::default(),
+			p2pool: P2pool::default(),
+			xmrig: Xmrig::with_threads(max_threads, current_threads),
+			version: Arc::new(Mutex::new(Version::default())),
 		}
 	}
 
@@ -237,6 +173,14 @@ impl State {
 		}
 	}
 
+	// Conver [State] to [String]
+	pub fn to_string(&self) -> Result<String, TomlError> {
+		match toml::ser::to_string(self) {
+			Ok(s) => Ok(s),
+			Err(e) => { error!("State | Couldn't serialize default file: {}", e); Err(TomlError::Serialize(e)) },
+		}
+	}
+
 	// Combination of multiple functions:
 	//   1. Attempt to read file from path into [String]
 	//      |_ Create a default file if not found
@@ -261,7 +205,10 @@ impl State {
 			Ok(s) => Ok(s),
 			Err(_) => {
 				warn!("State | Attempting merge...");
-				Self::merge(string, path)
+				match Self::merge(&string) {
+					Ok(mut new) => { Self::save(&mut new, path)?; Ok(new) },
+					Err(e)  => Err(e),
+				}
 			},
 		}
 	}
@@ -271,10 +218,7 @@ impl State {
 	pub fn create_new(path: &PathBuf) -> Result<Self, TomlError> {
 		info!("State | Creating new default...");
 		let new = Self::new();
-		let string = match toml::ser::to_string(&new) {
-				Ok(o) => o,
-				Err(e) => { error!("State | Couldn't serialize default file: {}", e); return Err(TomlError::Serialize(e)) },
-		};
+		let string = Self::to_string(&new)?;
 		fs::write(path, string)?;
 		info!("State | Write ... OK");
 		Ok(new)
@@ -302,18 +246,12 @@ impl State {
 
 	// Take [String] as input, merge it with whatever the current [default] is,
 	// leaving behind old keys+values and updating [default] with old valid ones.
-	// Automatically overwrite current file.
-	pub fn merge(old: String, path: &PathBuf) -> Result<Self, TomlError> {
-		let default = match toml::ser::to_string(&Self::new()) {
-			Ok(string) => { info!("State | Default TOML parse ... OK"); string },
-			Err(err) => { error!("State | Couldn't parse default TOML into string"); return Err(TomlError::Serialize(err)) },
-		};
-		let mut new: Self = match Figment::new().merge(Toml::string(&old)).merge(Toml::string(&default)).extract() {
+	pub fn merge(old: &str) -> Result<Self, TomlError> {
+		let default = toml::ser::to_string(&Self::new()).unwrap();
+		let new: Self = match Figment::from(Toml::string(&default)).merge(Toml::string(&old)).extract() {
 			Ok(new) => { info!("State | TOML merge ... OK"); new },
 			Err(err) => { error!("State | Couldn't merge default + old TOML"); return Err(TomlError::Merge(err)) },
 		};
-		// Attempt save
-		Self::save(&mut new, path)?;
 		Ok(new)
 	}
 }
@@ -705,3 +643,322 @@ pub struct Version {
 	pub p2pool: String,
 	pub xmrig: String,
 }
+
+//---------------------------------------------------------------------------------------------------- [State] Defaults
+impl Default for Gupax {
+	fn default() -> Self {
+		Self {
+			simple: true,
+			auto_update: true,
+			auto_p2pool: false,
+			auto_xmrig: false,
+			ask_before_quit: true,
+			save_before_quit: true,
+			#[cfg(not(target_os = "macos"))]
+			update_via_tor: true,
+			#[cfg(target_os = "macos")] // Arti library has issues on macOS
+			update_via_tor: false,
+			p2pool_path: DEFAULT_P2POOL_PATH.to_string(),
+			xmrig_path: DEFAULT_XMRIG_PATH.to_string(),
+			absolute_p2pool_path: into_absolute_path(DEFAULT_P2POOL_PATH.to_string()).unwrap(),
+			absolute_xmrig_path: into_absolute_path(DEFAULT_XMRIG_PATH.to_string()).unwrap(),
+			selected_width: APP_DEFAULT_WIDTH as u16,
+			selected_height: APP_DEFAULT_HEIGHT as u16,
+			ratio: Ratio::Width,
+			tab: Tab::About,
+		}
+	}
+}
+impl Default for P2pool {
+	fn default() -> Self {
+		Self {
+			simple: true,
+			mini: true,
+			auto_ping: true,
+			auto_select: true,
+			out_peers: 10,
+			in_peers: 10,
+			log_level: 3,
+			node: crate::NodeEnum::C3pool,
+			arguments: String::new(),
+			address: String::with_capacity(96),
+			name: "Local Monero Node".to_string(),
+			ip: "localhost".to_string(),
+			rpc: "18081".to_string(),
+			zmq: "18083".to_string(),
+			selected_index: 0,
+			selected_name: "Local Monero Node".to_string(),
+			selected_ip: "localhost".to_string(),
+			selected_rpc: "18081".to_string(),
+			selected_zmq: "18083".to_string(),
+		}
+	}
+}
+impl Xmrig {
+	fn with_threads(max_threads: usize, current_threads: usize) -> Self {
+		let mut xmrig = Self::default();
+		Self {
+			max_threads,
+			current_threads,
+			..xmrig
+		}
+	}
+}
+impl Default for Xmrig {
+	fn default() -> Self {
+		Self {
+			simple: true,
+			pause: 0,
+			simple_rig: String::with_capacity(30),
+			arguments: String::with_capacity(300),
+			address: String::with_capacity(96),
+			name: "Local P2Pool".to_string(),
+			rig: GUPAX_VERSION_UNDERSCORE.to_string(),
+			ip: "localhost".to_string(),
+			port: "3333".to_string(),
+			selected_index: 0,
+			selected_name: "Local P2Pool".to_string(),
+			selected_ip: "localhost".to_string(),
+			selected_rig: GUPAX_VERSION_UNDERSCORE.to_string(),
+			selected_port: "3333".to_string(),
+			api_ip: "localhost".to_string(),
+			api_port: "18088".to_string(),
+			tls: false,
+			keepalive: false,
+			current_threads: 1,
+			max_threads: 1,
+		}
+	}
+}
+impl Default for Version {
+	fn default() -> Self {
+		Self {
+			gupax: GUPAX_VERSION.to_string(),
+			p2pool: P2POOL_VERSION.to_string(),
+			xmrig: XMRIG_VERSION.to_string(),
+		}
+	}
+}
+
+//---------------------------------------------------------------------------------------------------- TESTS
+#[cfg(test)]
+mod test {
+	#[test]
+	fn serde_default_state() {
+		let state = crate::State::new();
+		let string = crate::State::to_string(&state).unwrap();
+		crate::State::from_str(&string).unwrap();
+	}
+	#[test]
+	fn serde_default_node() {
+		let node = crate::Node::new_vec();
+		let string = crate::Node::to_string(&node).unwrap();
+		crate::Node::from_str_to_vec(&string).unwrap();
+	}
+	#[test]
+	fn serde_default_pool() {
+		let pool = crate::Pool::new_vec();
+		let string = crate::Pool::to_string(&pool).unwrap();
+		crate::Pool::from_str_to_vec(&string).unwrap();
+	}
+
+	#[test]
+	fn serde_custom_state() {
+		let state = r#"
+			[gupax]
+			simple = true
+			auto_update = true
+			auto_p2pool = false
+			auto_xmrig = false
+			ask_before_quit = true
+			save_before_quit = true
+			update_via_tor = true
+			p2pool_path = "p2pool/p2pool"
+			xmrig_path = "xmrig/xmrig"
+			absolute_p2pool_path = "/home/hinto/p2pool/p2pool"
+			absolute_xmrig_path = "/home/hinto/xmrig/xmrig"
+			selected_width = 1280
+			selected_height = 960
+			tab = "About"
+			ratio = "Width"
+
+			[p2pool]
+			simple = true
+			mini = true
+			auto_ping = true
+			auto_select = true
+			out_peers = 10
+			in_peers = 450
+			log_level = 3
+			node = "Seth"
+			arguments = ""
+			address = "44hintoFpuo3ugKfcqJvh5BmrsTRpnTasJmetKC4VXCt6QDtbHVuixdTtsm6Ptp7Y8haXnJ6j8Gj2dra8CKy5ewz7Vi9CYW"
+			name = "Local Monero Node"
+			ip = "192.168.1.123"
+			rpc = "18089"
+			zmq = "18083"
+			selected_index = 0
+			selected_name = "Local Monero Node"
+			selected_ip = "192.168.1.123"
+			selected_rpc = "18089"
+			selected_zmq = "18083"
+
+			[xmrig]
+			simple = true
+			pause = 0
+			simple_rig = ""
+			arguments = ""
+			tls = false
+			keepalive = false
+			max_threads = 32
+			current_threads = 16
+			address = ""
+			api_ip = "localhost"
+			api_port = "18088"
+			name = "linux"
+			rig = "Gupax"
+			ip = "192.168.1.122"
+			port = "3333"
+			selected_index = 1
+			selected_name = "linux"
+			selected_rig = "Gupax"
+			selected_ip = "192.168.1.122"
+			selected_port = "3333"
+
+			[version]
+			gupax = "v1.0.0"
+			p2pool = "v2.5"
+			xmrig = "v6.18.0"
+		"#;
+		let state = crate::State::from_str(state).unwrap();
+		crate::State::to_string(&state).unwrap();
+	}
+
+	#[test]
+	fn serde_custom_node() {
+		let node = r#"
+			['Local Monero Node']
+			ip = "localhost"
+			rpc = "18081"
+			zmq = "18083"
+
+			['asdf-_. ._123']
+			ip = "localhost"
+			rpc = "11"
+			zmq = "1234"
+
+			['aaa     bbb']
+			ip = "192.168.2.333"
+			rpc = "1"
+			zmq = "65535"
+		"#;
+		let node = crate::Node::from_str_to_vec(node).unwrap();
+		crate::Node::to_string(&node).unwrap();
+	}
+
+	#[test]
+	fn serde_custom_pool() {
+		let pool = r#"
+			['Local P2Pool']
+			rig = "Gupax_v1.0.0"
+			ip = "localhost"
+			port = "3333"
+
+			['aaa xx .. -']
+			rig = "Gupax"
+			ip = "192.168.22.22"
+			port = "1"
+
+			['           a']
+			rig = "Gupax_v1.0.0"
+			ip = "127.0.0.1"
+			port = "65535"
+		"#;
+		let pool = crate::Pool::from_str_to_vec(pool).unwrap();
+		crate::Pool::to_string(&pool).unwrap();
+	}
+
+	// Make sure we keep the user's old values that are still
+	// valid but discard the ones that don't exist anymore.
+	#[test]
+	fn merge_state() {
+		let bad_state = r#"
+			[gupax]
+			SETTING_THAT_DOESNT_EXIST_ANYMORE = 123123
+			simple = false
+			auto_update = true
+			auto_p2pool = false
+			auto_xmrig = false
+			ask_before_quit = true
+			save_before_quit = true
+			update_via_tor = true
+			p2pool_path = "p2pool/p2pool"
+			xmrig_path = "xmrig/xmrig"
+			absolute_p2pool_path = ""
+			absolute_xmrig_path = ""
+			selected_width = 0
+			selected_height = 0
+			tab = "About"
+			ratio = "Width"
+
+			[p2pool]
+			SETTING_THAT_DOESNT_EXIST_ANYMORE = "String"
+			simple = true
+			mini = true
+			auto_ping = true
+			auto_select = true
+			out_peers = 10
+			in_peers = 450
+			log_level = 6
+			node = "Seth"
+			arguments = ""
+			address = "44hintoFpuo3ugKfcqJvh5BmrsTRpnTasJmetKC4VXCt6QDtbHVuixdTtsm6Ptp7Y8haXnJ6j8Gj2dra8CKy5ewz7Vi9CYW"
+			name = "Local Monero Node"
+			ip = "localhost"
+			rpc = "18081"
+			zmq = "18083"
+			selected_index = 0
+			selected_name = "Local Monero Node"
+			selected_ip = "localhost"
+			selected_rpc = "18081"
+			selected_zmq = "18083"
+
+			[xmrig]
+			SETTING_THAT_DOESNT_EXIST_ANYMORE = true
+			simple = true
+			pause = 0
+			simple_rig = ""
+			arguments = ""
+			tls = false
+			keepalive = false
+			max_threads = 32
+			current_threads = 16
+			address = ""
+			api_ip = "localhost"
+			api_port = "18088"
+			name = "Local P2Pool"
+			rig = "Gupax_v1.0.0"
+			ip = "localhost"
+			port = "3333"
+			selected_index = 0
+			selected_name = "Local P2Pool"
+			selected_rig = "Gupax_v1.0.0"
+			selected_ip = "localhost"
+			selected_port = "3333"
+
+			[version]
+			gupax = "v1.0.0"
+			p2pool = "v2.5"
+			xmrig = "v6.18.0"
+		"#.to_string();
+		let merged_state = crate::State::merge(&bad_state).unwrap();
+		let merged_state = crate::State::to_string(&merged_state).unwrap();
+		println!("{}", merged_state);
+		assert!(merged_state.contains("simple = false"));
+		assert!(merged_state.contains("in_peers = 450"));
+		assert!(merged_state.contains("log_level = 6"));
+		assert!(merged_state.contains(r#"node = "Seth""#));
+		assert!(!merged_state.contains("SETTING_THAT_DOESNT_EXIST_ANYMORE"));
+		assert!(merged_state.contains("44hintoFpuo3ugKfcqJvh5BmrsTRpnTasJmetKC4VXCt6QDtbHVuixdTtsm6Ptp7Y8haXnJ6j8Gj2dra8CKy5ewz7Vi9CYW"));
+	}
+}
diff --git a/src/helper.rs b/src/helper.rs
index d126d9b..4484948 100644
--- a/src/helper.rs
+++ b/src/helper.rs
@@ -1713,3 +1713,179 @@ impl Hashrate {
 		}
 	}
 }
+
+//---------------------------------------------------------------------------------------------------- TESTS
+#[cfg(test)]
+mod test {
+	#[test]
+	fn calc_payouts_and_xmr_from_output_p2pool() {
+		use crate::helper::{PubP2poolApi,P2poolRegex};
+		use std::sync::{Arc,Mutex};
+		let public = Arc::new(Mutex::new(PubP2poolApi::new()));
+		let output_parse = Arc::new(Mutex::new(String::from(
+			r#"You received a payout of 5.000000000001 XMR in block 1111
+			You received a payout of 5.000000000001 XMR in block 1112
+			You received a payout of 5.000000000001 XMR in block 1113"#
+		)));
+		let output_pub = Arc::new(Mutex::new(String::new()));
+		let elapsed = std::time::Duration::from_secs(60);
+		let regex = P2poolRegex::new();
+		PubP2poolApi::update_from_output(&public, &output_parse, &output_pub, elapsed, &regex);
+		let public = public.lock().unwrap();
+		println!("{:#?}", public);
+		assert_eq!(public.payouts,       3);
+		assert_eq!(public.payouts_hour,  180.0);
+		assert_eq!(public.payouts_day,   4320.0);
+		assert_eq!(public.payouts_month, 129600.0);
+		assert_eq!(public.xmr,           15.000000000003);
+		assert_eq!(public.xmr_hour,      900.00000000018);
+		assert_eq!(public.xmr_day,       21600.00000000432);
+		assert_eq!(public.xmr_month,     648000.0000001296);
+	}
+
+	#[test]
+	fn serde_priv_p2pool_api() {
+		let data =
+			r#"{
+				"hashrate_15m": 12,
+				"hashrate_1h": 11111,
+				"hashrate_24h": 468967,
+				"total_hashes": 2019283840922394082390,
+				"shares_found": 289037,
+				"average_effort": 915.563,
+				"current_effort": 129.297,
+				"connections": 123,
+				"incoming_connections": 96
+			}"#;
+		use crate::helper::PrivP2poolApi;
+		let priv_api = PrivP2poolApi::str_to_priv_p2pool_api(data).unwrap();
+		let json = serde_json::ser::to_string_pretty(&priv_api).unwrap();
+		println!("{}", json);
+		let data_after_ser =
+r#"{
+  "hashrate_15m": 12,
+  "hashrate_1h": 11111,
+  "hashrate_24h": 468967,
+  "shares_found": 289037,
+  "average_effort": 915.563,
+  "current_effort": 129.297,
+  "connections": 123
+}"#;
+		assert_eq!(data_after_ser, json)
+	}
+
+	#[test]
+	fn serde_priv_xmrig_api() {
+		let data =
+		r#"{
+		    "id": "6226e3sd0cd1a6es",
+		    "worker_id": "hinto",
+		    "uptime": 123,
+		    "restricted": true,
+		    "resources": {
+		        "memory": {
+		            "free": 123,
+		            "total": 123123,
+		            "resident_set_memory": 123123123
+		        },
+		        "load_average": [10.97, 10.58, 10.47],
+		        "hardware_concurrency": 12
+		    },
+		    "features": ["api", "asm", "http", "hwloc", "tls", "opencl", "cuda"],
+		    "results": {
+		        "diff_current": 123,
+		        "shares_good": 123,
+		        "shares_total": 123,
+		        "avg_time": 123,
+		        "avg_time_ms": 123,
+		        "hashes_total": 123,
+		        "best": [123, 123, 123, 13, 123, 123, 123, 123, 123, 123],
+		        "error_log": []
+		    },
+		    "algo": "rx/0",
+		    "connection": {
+		        "pool": "localhost:3333",
+		        "ip": "127.0.0.1",
+		        "uptime": 123,
+		        "uptime_ms": 123,
+		        "ping": 0,
+		        "failures": 0,
+		        "tls": null,
+		        "tls-fingerprint": null,
+		        "algo": "rx/0",
+		        "diff": 123,
+		        "accepted": 123,
+		        "rejected": 123,
+		        "avg_time": 123,
+		        "avg_time_ms": 123,
+		        "hashes_total": 123,
+		        "error_log": []
+		    },
+		    "version": "6.18.0",
+		    "kind": "miner",
+		    "ua": "XMRig/6.18.0 (Linux x86_64) libuv/2.0.0-dev gcc/10.2.1",
+		    "cpu": {
+		        "brand": "blah blah blah",
+		        "family": 1,
+		        "model": 2,
+		        "stepping": 0,
+		        "proc_info": 123,
+		        "aes": true,
+		        "avx2": true,
+		        "x64": true,
+		        "64_bit": true,
+		        "l2": 123123,
+		        "l3": 123123,
+		        "cores": 12,
+		        "threads": 24,
+		        "packages": 1,
+		        "nodes": 1,
+		        "backend": "hwloc/2.8.0a1-git",
+		        "msr": "ryzen_19h",
+		        "assembly": "ryzen",
+		        "arch": "x86_64",
+		        "flags": ["aes", "vaes", "avx", "avx2", "bmi2", "osxsave", "pdpe1gb", "sse2", "ssse3", "sse4.1", "popcnt", "cat_l3"]
+		    },
+		    "donate_level": 0,
+		    "paused": false,
+		    "algorithms": ["cn/1", "cn/2", "cn/r", "cn/fast", "cn/half", "cn/xao", "cn/rto", "cn/rwz", "cn/zls", "cn/double", "cn/ccx", "cn-lite/1", "cn-heavy/0", "cn-heavy/tube", "cn-heavy/xhv", "cn-pico", "cn-pico/tlo", "cn/upx2", "rx/0", "rx/wow", "rx/arq", "rx/graft", "rx/sfx", "rx/keva", "argon2/chukwa", "argon2/chukwav2", "argon2/ninja", "astrobwt", "astrobwt/v2", "ghostrider"],
+		    "hashrate": {
+		        "total": [111.11, 111.11, 111.11],
+		        "highest": 111.11,
+		        "threads": [
+		            [111.11, 111.11, 111.11]
+		        ]
+		    },
+		    "hugepages": true
+		}"#;
+		use crate::helper::PrivXmrigApi;
+		let priv_api = serde_json::from_str::<PrivXmrigApi>(&data).unwrap();
+		let json = serde_json::ser::to_string_pretty(&priv_api).unwrap();
+		println!("{}", json);
+		let data_after_ser =
+r#"{
+  "worker_id": "hinto",
+  "resources": {
+    "load_average": [
+      10.97,
+      10.58,
+      10.47
+    ]
+  },
+  "connection": {
+    "pool": "localhost:3333",
+    "diff": 123,
+    "accepted": 123,
+    "rejected": 123
+  },
+  "hashrate": {
+    "total": [
+      111.11,
+      111.11,
+      111.11
+    ]
+  }
+}"#;
+		assert_eq!(data_after_ser, json)
+	}
+}
diff --git a/src/main.rs b/src/main.rs
index c5160a7..9937cc2 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -471,6 +471,7 @@ pub struct ErrorState {
 	msg: String, // What message to display?
 	ferris: ErrorFerris, // Which ferris to display?
 	buttons: ErrorButtons, // Which buttons to display?
+	quit_twice: bool, // This indicates the user tried to quit on the [ask_before_quit] screen
 }
 
 impl Default for ErrorState {
@@ -486,6 +487,7 @@ impl ErrorState {
 			msg: "Unknown Error".to_string(),
 			ferris: ErrorFerris::Oops,
 			buttons: ErrorButtons::Okay,
+			quit_twice: false,
 		}
 	}
 
@@ -506,6 +508,7 @@ impl ErrorState {
 			msg: msg.into(),
 			ferris,
 			buttons,
+			quit_twice: false,
 		};
 	}
 
@@ -524,6 +527,7 @@ impl ErrorState {
 			msg: String::new(),
 			ferris: ErrorFerris::Sudo,
 			buttons: ErrorButtons::Sudo,
+			quit_twice: false,
 		};
 		SudoState::reset(state)
 	}
@@ -914,14 +918,16 @@ fn main() {
 
 impl eframe::App for App {
 	fn on_close_event(&mut self) -> bool {
-		// If we're already on the [ask_before_quit] screen and
-		// the user tries to exit again, exit.
-		if self.error_state.buttons == ErrorButtons::StayQuit {
-			if self.state.gupax.save_before_quit { self.save_before_quit(); }
-			true
-		// Else, set up the [ask_before_quit] screen (if enabled).
-		} else if self.state.gupax.ask_before_quit {
+		if self.state.gupax.ask_before_quit {
+			// If we're already on the [ask_before_quit] screen and
+			// the user tried to exit again, exit.
+			if self.error_state.quit_twice {
+				if self.state.gupax.save_before_quit { self.save_before_quit(); }
+				return true
+			}
+			// Else, set the error
 			self.error_state.set("", ErrorFerris::Oops, ErrorButtons::StayQuit);
+			self.error_state.quit_twice = true;
 			false
 		// Else, just quit.
 		} else {
@@ -1553,3 +1559,23 @@ impl eframe::App for App {
 		});
 	}
 }
+
+//---------------------------------------------------------------------------------------------------- TESTS
+#[cfg(test)]
+mod test {
+	#[test]
+	fn build_app() {
+		let mut app = crate::App::new(std::time::Instant::now());
+		crate::init_auto(&mut app);
+	}
+
+	#[test]
+	fn build_regex() {
+		crate::Regexes::new();
+	}
+
+	#[test]
+	fn build_images() {
+		crate::Images::new();
+	}
+}
diff --git a/src/node.rs b/src/node.rs
index f3b2ea9..eb4ef1f 100644
--- a/src/node.rs
+++ b/src/node.rs
@@ -58,6 +58,7 @@ pub const NODE_IPS: [&str; 17] = [
 ];
 
 pub const COMMUNITY_NODE_LENGTH: usize = NODE_IPS.len();
+pub const COMMUNITY_NODE_MAX_CHARS: usize = 14;
 
 #[derive(Copy,Clone,Eq,PartialEq,Debug,Deserialize,Serialize)]
 pub enum NodeEnum {
@@ -65,7 +66,17 @@ pub enum NodeEnum {
 	Plowsof2,Rino,Feather1,Feather2,Seth,SupportXmr,SupportXmrIr,XmrVsBeast,
 }
 
+impl Default for NodeEnum {
+	fn default() -> Self {
+		Self::new()
+	}
+}
+
 impl NodeEnum {
+	fn new() -> Self {
+		ip_to_enum(NODE_IPS[0])
+	}
+
 	fn get_index(&self) -> usize {
 		match self {
 			C3pool         => 0,
@@ -280,7 +291,7 @@ impl Ping {
 	pub fn new() -> Self {
 		Self {
 			nodes: NodeData::new_vec(),
-			fastest: NodeEnum::C3pool,
+			fastest: NodeEnum::new(),
 			pinging: false,
 			msg: "No ping in progress".to_string(),
 			prog: 0.0,
@@ -417,3 +428,23 @@ impl Ping {
 		node_vec.lock().unwrap().push(NodeData { id: ip_to_enum(ip), ip, ms, color, });
 	}
 }
+
+//---------------------------------------------------------------------------------------------------- TESTS
+#[cfg(test)]
+mod test {
+	#[test]
+	fn validate_node_ips() {
+		for ip in crate::NODE_IPS {
+			assert!(ip.len() < 255);
+			assert!(ip.is_ascii());
+			assert!(ip.ends_with(":18081") || ip.ends_with(":18089"));
+		}
+	}
+
+	#[test]
+	fn spacing() {
+		for ip in crate::NODE_IPS {
+			assert!(crate::format_enum(crate::ip_to_enum(ip)).len() == crate::COMMUNITY_NODE_MAX_CHARS);
+		}
+	}
+}