From 5ec3a6006f24260ecff4175776ef402ea72ca330 Mon Sep 17 00:00:00 2001 From: whilb Date: Mon, 1 Sep 2025 20:07:20 -0700 Subject: [PATCH] zfs --- public/calculators/zfs.js | 390 ++++++++++++++ public/js/app.js | 1 + tests/test_zfs.py | 1028 +++++++++++++++++++++++++++++++++++++ 3 files changed, 1419 insertions(+) create mode 100644 public/calculators/zfs.js create mode 100644 tests/test_zfs.py diff --git a/public/calculators/zfs.js b/public/calculators/zfs.js new file mode 100644 index 0000000..c96e420 --- /dev/null +++ b/public/calculators/zfs.js @@ -0,0 +1,390 @@ +import {revive, persist, labelInput, labelSelect} from '/js/util.js'; + +export default { + id:'zfs', name:'ZFS Calculator', about:'Calculate ZFS pool configurations, performance tuning, and capacity planning.', + render(root){ + const key='calc_zfs_v1'; + const s = revive(key,{ + poolType: 'raidz2', + diskCount: 6, + diskSize: 4000, + diskSizeUnit: 'GB', + blockSize: '128K', + compression: 'lz4', + dedup: false, + ashift: 12, + arcMax: 8192, + arcMaxUnit: 'MB' + }); + + const ui = document.createElement('div'); + + // Pool configuration section + const poolSection = document.createElement('div'); + poolSection.innerHTML = ` +

Pool Configuration

+
+
+ + +
+
+ + +
+
+ +
+ + +
+
+
+ + +
+
+ `; + + // Performance tuning section + const perfSection = document.createElement('div'); + perfSection.innerHTML = ` +

Performance Tuning

+
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ + +
+
+
+ `; + + ui.append(poolSection, perfSection); + + // Results section + const out = document.createElement('div'); + out.className = 'result'; + out.style.cssText = ` + margin: 20px 0; + padding: 15px; + background: var(--k-bg); + border-radius: 8px; + border-left: 4px solid var(--accent); + `; + ui.append(out); + + + // Calculation function + function calc(){ + const poolType = ui.querySelector('[name=poolType]').value; + const diskCount = +ui.querySelector('[name=diskCount]').value; + const diskSize = +ui.querySelector('[name=diskSize]').value; + const diskSizeUnit = ui.querySelector('[name=diskSizeUnit]').value; + const blockSize = ui.querySelector('[name=blockSize]').value; + const compression = ui.querySelector('[name=compression]').value; + const dedup = ui.querySelector('[name=dedup]').value === 'true'; + const ashift = +ui.querySelector('[name=ashift]').value; + const arcMax = +ui.querySelector('[name=arcMax]').value; + const arcMaxUnit = ui.querySelector('[name=arcMaxUnit]').value; + + // Convert disk size to GB for calculations + let diskSizeGB = diskSize; + if (diskSizeUnit === 'TB') { + diskSizeGB = diskSize * 1024; + } + + // Calculate usable capacity based on pool type + let usableCapacity, redundancy, minDisks, recommendedDisks, vdevCount; + + switch(poolType) { + case 'stripe': + usableCapacity = diskCount * diskSizeGB; + redundancy = 'None'; + minDisks = 1; + recommendedDisks = 1; + vdevCount = 1; + break; + case 'mirror': + usableCapacity = Math.floor(diskCount / 2) * diskSizeGB; + redundancy = '50%'; + minDisks = 2; + recommendedDisks = 2; + vdevCount = Math.floor(diskCount / 2); + break; + case 'raidz1': + usableCapacity = (diskCount - 1) * diskSizeGB; + redundancy = '1 disk'; + minDisks = 3; + recommendedDisks = 3; + vdevCount = 1; + break; + case 'raidz2': + usableCapacity = (diskCount - 2) * diskSizeGB; + redundancy = '2 disks'; + minDisks = 4; + recommendedDisks = 6; + vdevCount = 1; + break; + case 'raidz3': + usableCapacity = (diskCount - 3) * diskSizeGB; + redundancy = '3 disks'; + minDisks = 5; + recommendedDisks = 9; + vdevCount = 1; + break; + case 'mirror2x2': + usableCapacity = 2 * diskSizeGB; // 2 mirrors, each with 1 usable disk + redundancy = '50%'; + minDisks = 4; + recommendedDisks = 4; + vdevCount = 2; + break; + case 'mirror3x2': + usableCapacity = 3 * diskSizeGB; // 3 mirrors, each with 1 usable disk + redundancy = '50%'; + minDisks = 6; + recommendedDisks = 6; + vdevCount = 3; + break; + case 'raidz2x2': + usableCapacity = 2 * (diskCount / 2 - 2) * diskSizeGB; // 2 RAID-Z2 vdevs + redundancy = '2 disks per vdev'; + minDisks = 8; + recommendedDisks = 12; + vdevCount = 2; + break; + } + + // Validate disk count + if (diskCount < minDisks) { + out.innerHTML = `
+ Error: ${poolType.toUpperCase()} requires at least ${minDisks} disks +
`; + return; + } + + // Calculate compression ratios + const compressionRatios = { + 'off': 1.0, + 'lz4': 2.1, + 'gzip': 2.5, + 'gzip-1': 2.0, + 'gzip-9': 3.0, + 'zstd': 2.8, + 'zstd-1': 2.2, + 'zstd-19': 3.5 + }; + + const compressionRatio = compressionRatios[compression] || 1.0; + const effectiveCapacity = usableCapacity * compressionRatio; + + // Calculate performance metrics + const blockSizeKB = parseInt(blockSize.replace(/[^0-9]/g, '')); + const ashiftBytes = Math.pow(2, ashift); + + // Convert ARC max to MB + let arcMaxMB = arcMax; + if (arcMaxUnit === 'GB') { + arcMaxMB = arcMax * 1024; + } + + // Helper function to format capacity + function formatCapacity(gb) { + if (gb >= 1024) { + return `${gb.toFixed(1)} GB (${(gb / 1024).toFixed(2)} TB)`; + } + return `${gb.toFixed(1)} GB`; + } + + // Calculate I/O performance estimates + const estimatedIOPS = Math.floor(diskCount * 100); // Rough estimate: 100 IOPS per disk + const estimatedThroughput = Math.floor(diskCount * 150); // Rough estimate: 150 MB/s per disk + + // Calculate memory requirements (rule of thumb: 1GB RAM per 1TB storage) + const recommendedRAM = Math.ceil((diskCount * diskSizeGB) / 1024); + + // Generate results + out.innerHTML = ` +
+ ZFS Pool Configuration +
+ +
+
+

Capacity & Redundancy

+
+ Raw Capacity: ${formatCapacity(diskCount * diskSizeGB)} +
+
+ Usable Capacity: ${formatCapacity(usableCapacity)} +
+
+ Effective Capacity (with compression): ${formatCapacity(effectiveCapacity)} +
+
+ Redundancy: ${redundancy} +
+
+ Efficiency: ${((usableCapacity / (diskCount * diskSizeGB)) * 100).toFixed(1)}% +
+
+ Compression Savings: ${((effectiveCapacity - usableCapacity) / usableCapacity * 100).toFixed(1)}% +
+
+ VDev Count: ${vdevCount} ${vdevCount > 1 ? 'vdevs' : 'vdev'} +
+
+ +
+

Performance Settings

+
+ Block Size: ${blockSize} +
+
+ Ashift: ${ashift} (${ashiftBytes} bytes) +
+
+ Compression: ${compression} (${compressionRatio.toFixed(1)}x ratio) +
+
+ Deduplication: ${dedup ? 'On' : 'Off'} +
+
+ ARC Max: ${arcMaxMB} MB +
+
+
+ +
+
+

Performance Estimates

+
+ Estimated IOPS: ${estimatedIOPS.toLocaleString()} (random 4K reads) +
+
+ Estimated Throughput: ${estimatedThroughput} MB/s (sequential) +
+
+ Recommended RAM: ${recommendedRAM} GB minimum +
+
+ Current ARC: ${(arcMaxMB / 1024).toFixed(1)} GB +
+
+ +
+

System Requirements

+
+ Minimum RAM: ${Math.max(8, recommendedRAM)} GB +
+
+ Recommended RAM: ${Math.max(16, recommendedRAM * 2)} GB +
+
+ CPU Cores: ${Math.max(4, Math.ceil(diskCount / 2))} cores recommended +
+
+ Network: 10 Gbps recommended for ${estimatedThroughput} MB/s throughput +
+
+
+ + + + `; + + persist(key, {poolType, diskCount, diskSize, diskSizeUnit, blockSize, compression, dedup, ashift, arcMax, arcMaxUnit}); + } + + // Event listeners + ui.querySelector('[name=poolType]').addEventListener('change', calc); + ui.querySelector('[name=diskCount]').addEventListener('input', calc); + ui.querySelector('[name=diskSize]').addEventListener('input', calc); + ui.querySelector('[name=diskSizeUnit]').addEventListener('change', calc); + ui.querySelector('[name=blockSize]').addEventListener('change', calc); + ui.querySelector('[name=compression]').addEventListener('change', calc); + ui.querySelector('[name=dedup]').addEventListener('change', calc); + ui.querySelector('[name=ashift]').addEventListener('change', calc); + ui.querySelector('[name=arcMax]').addEventListener('input', calc); + ui.querySelector('[name=arcMaxUnit]').addEventListener('change', calc); + + // Initial calculation + calc(); + root.append(ui); + } +} diff --git a/public/js/app.js b/public/js/app.js index 1ebb89a..b009f43 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -6,6 +6,7 @@ const CALCS = [ { id:'bandwidth', name:'Bandwidth', about:'Bits↔bytes unit conv.', path:'../calculators/bandwidth.js' }, { id:'nmea', name:'NMEA', about:'0183 XOR checksum', path:'../calculators/nmea.js' }, { id:'currency', name:'Currency Converter', about:'Convert between currencies', path:'../calculators/currency.js' }, + { id:'zfs', name:'ZFS', about:'Pool configuration & performance', path:'../calculators/zfs.js' }, { id:'subnet', name:'IP Subnet', about:'IPv4/IPv6 subnet calculations', path:'../calculators/subnet.js' } ]; diff --git a/tests/test_zfs.py b/tests/test_zfs.py new file mode 100644 index 0000000..9a3a4c3 --- /dev/null +++ b/tests/test_zfs.py @@ -0,0 +1,1028 @@ +import pytest +from selenium.webdriver.common.by import By +from selenium.webdriver.support.ui import WebDriverWait +from selenium.webdriver.support import expected_conditions as EC +from selenium.webdriver.common.keys import Keys +import time + +@pytest.fixture +def zfs_page(driver, dev_server): + """Navigate to ZFS calculator page""" + driver.get(f"{dev_server}") + + # Wait for page to load and JavaScript to populate navigation + WebDriverWait(driver, 10).until( + EC.presence_of_element_located((By.CLASS_NAME, "sidenav")) + ) + # Wait for JavaScript to populate the navigation with calculator links + WebDriverWait(driver, 10).until( + EC.presence_of_element_located((By.XPATH, "//a[contains(text(), 'ZFS')]")) + ) + + # Click on ZFS Calculator + zfs_btn = driver.find_element(By.XPATH, "//a[contains(text(), 'ZFS')]") + zfs_btn.click() + + # Wait for ZFS calculator to load + WebDriverWait(driver, 10).until( + EC.presence_of_element_located((By.NAME, "poolType")) + ) + + return driver + +def _get_zfs_result(zfs_page): + """Helper to get the correct ZFS result element""" + result_elements = zfs_page.find_elements(By.CLASS_NAME, "result") + + # Look for the result element that contains ZFS-related content + zfs_result = None + for elem in result_elements: + html = elem.get_attribute('innerHTML') + if 'ZFS' in html or 'Pool Configuration' in html or 'Capacity' in html: + zfs_result = elem + break + + if not zfs_result: + # If no ZFS result found, use the last result element (most recent) + zfs_result = result_elements[-1] + + return zfs_result + +def test_zfs_calculator_loads(zfs_page): + """Test that the ZFS calculator loads correctly""" + # Wait a bit for JavaScript to fully load + time.sleep(2) + + # Check for key elements + assert zfs_page.find_element(By.NAME, "poolType") + assert zfs_page.find_element(By.NAME, "diskCount") + assert zfs_page.find_element(By.NAME, "diskSize") + assert zfs_page.find_element(By.NAME, "compression") + + # Find the displayed result element (should be the ZFS one) + displayed_result = None + result_elements = zfs_page.find_elements(By.CLASS_NAME, "result") + for elem in result_elements: + if elem.is_displayed(): + displayed_result = elem + break + + assert displayed_result is not None, "No displayed result element found" + + # Check that the ZFS result is shown + assert "ZFS Pool Configuration" in displayed_result.text + +def test_zfs_pool_type_selection(zfs_page): + """Test that pool type selection works and updates calculations""" + # Get initial result + initial_result = _get_zfs_result(zfs_page) + initial_text = initial_result.text + + # Change pool type to mirror using Select Lite + pool_type_button = zfs_page.find_element(By.CSS_SELECTOR, '[name="poolType"] + .select-lite__button') + pool_type_button.click() + + # Wait for dropdown to appear and click the mirror option + mirror_option = WebDriverWait(zfs_page, 10).until( + EC.element_to_be_clickable((By.XPATH, "//div[contains(@class, 'select-lite__option') and contains(text(), 'Mirror')]")) + ) + mirror_option.click() + + # Wait for calculation to update + time.sleep(1) + + # Check that result changed + updated_result = _get_zfs_result(zfs_page) + updated_text = updated_result.text + assert updated_text != initial_text + + # Check that the calculation reflects mirror configuration (50% efficiency for 2-way mirror) + assert "50.0%" in updated_text + assert "12000.0 GB" in updated_text # Half of 24000 GB for 2-way mirror + +def test_zfs_disk_count_validation(zfs_page): + """Test that disk count validation works correctly""" + # Set pool type to RAID-Z2 (requires at least 4 disks) using Select Lite + pool_type_button = zfs_page.find_element(By.CSS_SELECTOR, '[name="poolType"] + .select-lite__button') + pool_type_button.click() + + # Wait for dropdown to appear and click the RAID-Z2 option + raidz2_option = WebDriverWait(zfs_page, 10).until( + EC.element_to_be_clickable((By.XPATH, "//div[contains(@class, 'select-lite__option') and contains(text(), 'RAID-Z2')]")) + ) + raidz2_option.click() + + # Set disk count to 2 (invalid for RAID-Z2) + disk_count_input = zfs_page.find_element(By.NAME, "diskCount") + disk_count_input.clear() + disk_count_input.send_keys("2") + + # Wait for validation error + time.sleep(1) + + # Check for error message + result = _get_zfs_result(zfs_page) + assert "Error" in result.text + assert "RAIDZ2 requires at least 4 disks" in result.text + +def test_zfs_compression_calculation(zfs_page): + """Test that compression ratios are calculated correctly""" + # Set compression to LZ4 using Select Lite + compression_button = zfs_page.find_element(By.CSS_SELECTOR, '[name="compression"] + .select-lite__button') + compression_button.click() + + # Wait for dropdown to appear and click the LZ4 option + lz4_option = WebDriverWait(zfs_page, 10).until( + EC.element_to_be_clickable((By.XPATH, "//div[contains(@class, 'select-lite__option') and contains(text(), 'LZ4')]")) + ) + lz4_option.click() + + # Wait for calculation + time.sleep(1) + + # Check that LZ4 compression ratio is shown + result = _get_zfs_result(zfs_page) + assert "2.1x ratio" in result.text + +def test_zfs_disk_size_unit_conversion(zfs_page): + """Test that disk size unit conversion works""" + # Get initial result with GB + initial_result = _get_zfs_result(zfs_page) + initial_text = initial_result.text + + # Change to TB using Select Lite + disk_size_unit_button = zfs_page.find_element(By.CSS_SELECTOR, '[name="diskSizeUnit"] + .select-lite__button') + disk_size_unit_button.click() + + # Wait for dropdown to appear and click the TB option + tb_option = WebDriverWait(zfs_page, 10).until( + EC.element_to_be_clickable((By.XPATH, "//div[contains(@class, 'select-lite__option') and contains(text(), 'TB')]")) + ) + tb_option.click() + + # Wait for calculation + time.sleep(1) + + # Check that result changed (TB should show much larger numbers) + updated_result = _get_zfs_result(zfs_page) + updated_text = updated_result.text + assert updated_text != initial_text + +def test_zfs_block_size_selection(zfs_page): + """Test that block size selection works""" + # Change block size to 1M using Select Lite + block_size_button = zfs_page.find_element(By.CSS_SELECTOR, '[name="blockSize"] + .select-lite__button') + block_size_button.click() + + # Wait for dropdown to appear and click the 1M option + one_mb_option = WebDriverWait(zfs_page, 10).until( + EC.element_to_be_clickable((By.XPATH, "//div[contains(@class, 'select-lite__option') and contains(text(), '1M')]")) + ) + one_mb_option.click() + + # Wait for calculation + time.sleep(1) + + # Check that 1M block size is shown in results + result = _get_zfs_result(zfs_page) + assert "1M" in result.text + +def test_zfs_ashift_calculation(zfs_page): + """Test that ashift values are calculated correctly""" + # Set ashift to 13 using Select Lite + ashift_button = zfs_page.find_element(By.CSS_SELECTOR, '[name="ashift"] + .select-lite__button') + ashift_button.click() + + # Wait for dropdown to appear and click the ashift 13 option + ashift_13_option = WebDriverWait(zfs_page, 10).until( + EC.element_to_be_clickable((By.XPATH, "//div[contains(@class, 'select-lite__option') and contains(text(), '13 (8KB sectors)')]")) + ) + ashift_13_option.click() + + # Wait for calculation + time.sleep(1) + + # Check that ashift 13 and 8KB are shown + result = _get_zfs_result(zfs_page) + assert "13 (8192 bytes)" in result.text + +def test_zfs_arc_max_input(zfs_page): + """Test that ARC max size input works""" + # Change ARC max to 16384 MB + arc_max_input = zfs_page.find_element(By.NAME, "arcMax") + arc_max_input.clear() + arc_max_input.send_keys("16384") + + # Wait for calculation + time.sleep(1) + + # Check that 16384 MB is shown in results + result = _get_zfs_result(zfs_page) + assert "16384 MB" in result.text + +def test_zfs_dedup_warning(zfs_page): + """Test that deduplication warning is shown when enabled""" + # Enable deduplication using Select Lite + dedup_button = zfs_page.find_element(By.CSS_SELECTOR, '[name="dedup"] + .select-lite__button') + dedup_button.click() + + # Wait for dropdown to appear and click the dedup on option + dedup_on_option = WebDriverWait(zfs_page, 10).until( + EC.element_to_be_clickable((By.XPATH, "//div[contains(@class, 'select-lite__option') and contains(text(), 'On (Use with Caution)')]")) + ) + dedup_on_option.click() + + # Wait for calculation + time.sleep(1) + + # Check that calculator still works with dedup enabled + result = _get_zfs_result(zfs_page) + assert "ZFS Pool Configuration" in result.text + +def test_zfs_recommendations(zfs_page): + """Test that core calculator functionality works""" + # Check that core sections exist + result = _get_zfs_result(zfs_page) + assert "ZFS Pool Configuration" in result.text + assert "Capacity & Redundancy" in result.text + assert "Performance Settings" in result.text + +def test_zfs_capacity_calculations(zfs_page): + """Test that capacity calculations are accurate""" + # Set to RAID-Z2 with 6 disks of 4TB each using Select Lite + pool_type_button = zfs_page.find_element(By.CSS_SELECTOR, '[name="poolType"] + .select-lite__button') + pool_type_button.click() + + # Wait for dropdown to appear and click the RAID-Z2 option + raidz2_option = WebDriverWait(zfs_page, 10).until( + EC.element_to_be_clickable((By.XPATH, "//div[contains(@class, 'select-lite__option') and contains(text(), 'RAID-Z2')]")) + ) + raidz2_option.click() + + disk_count_input = zfs_page.find_element(By.NAME, "diskCount") + disk_count_input.clear() + disk_count_input.send_keys("6") + + disk_size_input = zfs_page.find_element(By.NAME, "diskSize") + disk_size_input.clear() + disk_size_input.send_keys("4") + + # Change to TB using Select Lite + disk_size_unit_button = zfs_page.find_element(By.CSS_SELECTOR, '[name="diskSizeUnit"] + .select-lite__button') + disk_size_unit_button.click() + + # Wait for dropdown to appear and click the TB option + tb_option = WebDriverWait(zfs_page, 10).until( + EC.element_to_be_clickable((By.XPATH, "//div[contains(@class, 'select-lite__option') and contains(text(), 'TB')]")) + ) + tb_option.click() + + # Wait for calculation + time.sleep(1) + + # Check capacity calculations + result = _get_zfs_result(zfs_page) + result_text = result.text + + # 6 disks * 4TB = 24TB raw capacity + assert "24.0 TB" in result_text or "24576.0 GB" in result_text + + # RAID-Z2 with 6 disks: (6-2) * 4TB = 16TB usable + assert "16.0 TB" in result_text or "16384.0 GB" in result_text + + # Efficiency should be 66.7% (16/24) + assert "66.7%" in result_text + +def test_zfs_new_pool_types(zfs_page): + """Test the new pool types (mirror2x2, mirror3x2, raidz2x2)""" + # Test mirror2x2 + pool_type_button = zfs_page.find_element(By.CSS_SELECTOR, '[name="poolType"] + .select-lite__button') + pool_type_button.click() + + # Wait for dropdown and select mirror2x2 + mirror2x2_option = WebDriverWait(zfs_page, 10).until( + EC.element_to_be_clickable((By.XPATH, "//div[contains(@class, 'select-lite__option') and contains(text(), 'Mirror 2x2')]")) + ) + mirror2x2_option.click() + + # Set disk count to 4 + disk_count_input = zfs_page.find_element(By.NAME, "diskCount") + disk_count_input.clear() + disk_count_input.send_keys("4") + + # Wait for calculation + time.sleep(1) + + result = _get_zfs_result(zfs_page) + result_text = result.text + + # Mirror2x2 with 4 disks should have ~8TB usable (2 mirrors of 4TB each, default size) + assert "7.81 TB" in result_text or "8000.0 GB" in result_text + assert "VDev Count: 2 vdevs" in result_text + +def test_zfs_performance_estimates(zfs_page): + """Test that performance estimates are displayed""" + # Use default settings + time.sleep(1) + + result = _get_zfs_result(zfs_page) + result_text = result.text + + # Should show performance estimates + assert "Performance Estimates" in result_text + assert "Estimated IOPS:" in result_text + assert "Estimated Throughput:" in result_text + assert "Recommended RAM:" in result_text + assert "System Requirements" in result_text + assert "Minimum RAM:" in result_text + assert "CPU Cores:" in result_text + assert "Network:" in result_text + + + +def test_zfs_capacity_formatting(zfs_page): + """Test that capacity is formatted with both GB and TB""" + # Set to a large size to trigger TB formatting + disk_size_input = zfs_page.find_element(By.NAME, "diskSize") + disk_size_input.clear() + disk_size_input.send_keys("2") + + # Change to TB + disk_size_unit_button = zfs_page.find_element(By.CSS_SELECTOR, '[name="diskSizeUnit"] + .select-lite__button') + disk_size_unit_button.click() + + tb_option = WebDriverWait(zfs_page, 10).until( + EC.element_to_be_clickable((By.XPATH, "//div[contains(@class, 'select-lite__option') and contains(text(), 'TB')]")) + ) + tb_option.click() + + time.sleep(1) + + result = _get_zfs_result(zfs_page) + result_text = result.text + + # Should show both GB and TB for large capacities + assert "GB (" in result_text and "TB)" in result_text + assert "Compression Savings:" in result_text + +def test_zfs_all_pool_types(zfs_page): + """Test all pool types for correct capacity calculations""" + pool_types = [ + ('stripe', 1, 'None', 1, 1), + ('mirror', 2, '50%', 2, 2), + ('raidz1', 3, '1 disk', 3, 3), + ('raidz2', 4, '2 disks', 4, 6), + ('raidz3', 5, '3 disks', 5, 9), + ('mirror2x2', 4, '50%', 4, 4), + ('mirror3x2', 6, '50%', 6, 6), + ('raidz2x2', 8, '2 disks per vdev', 8, 12) + ] + + for pool_type, min_disks, redundancy, recommended_min, recommended_optimal in pool_types: + # Select pool type + pool_type_button = zfs_page.find_element(By.CSS_SELECTOR, '[name="poolType"] + .select-lite__button') + pool_type_button.click() + + # Find and click the specific pool type option + if pool_type == 'stripe': + xpath = "//div[contains(@class, 'select-lite__option') and contains(text(), 'Stripe')]" + elif pool_type == 'mirror': + xpath = "//div[contains(@class, 'select-lite__option') and contains(text(), 'Mirror (2-way)')]" + elif pool_type == 'raidz1': + xpath = "//div[contains(@class, 'select-lite__option') and contains(text(), 'RAID-Z1')]" + elif pool_type == 'raidz2': + xpath = "//div[contains(@class, 'select-lite__option') and contains(text(), 'RAID-Z2')]" + elif pool_type == 'raidz3': + xpath = "//div[contains(@class, 'select-lite__option') and contains(text(), 'RAID-Z3')]" + elif pool_type == 'mirror2x2': + xpath = "//div[contains(@class, 'select-lite__option') and contains(text(), 'Mirror 2x2')]" + elif pool_type == 'mirror3x2': + xpath = "//div[contains(@class, 'select-lite__option') and contains(text(), 'Mirror 3x2')]" + elif pool_type == 'raidz2x2': + xpath = "//div[contains(@class, 'select-lite__option') and contains(text(), 'RAID-Z2 2x2')]" + + pool_option = WebDriverWait(zfs_page, 10).until( + EC.element_to_be_clickable((By.XPATH, xpath)) + ) + pool_option.click() + + # Set appropriate disk count + disk_count_input = zfs_page.find_element(By.NAME, "diskCount") + disk_count_input.clear() + disk_count_input.send_keys(str(min_disks)) + + time.sleep(1) + + result = _get_zfs_result(zfs_page) + result_text = result.text + + # Verify redundancy is correct + assert redundancy in result_text, f"Pool type {pool_type} should show redundancy: {redundancy}" + + # Verify no error for minimum disk count + assert "Error" not in result_text, f"Pool type {pool_type} should not show error with {min_disks} disks" + +def test_zfs_compression_algorithms(zfs_page): + """Test all compression algorithms for correct ratios""" + compression_tests = [ + ('off', '1.0x ratio'), + ('lz4', '2.1x ratio'), + ('gzip', '2.5x ratio'), + ('gzip-1', '2.0x ratio'), + ('gzip-9', '3.0x ratio'), + ('zstd', '2.8x ratio'), + ('zstd-1', '2.2x ratio'), + ('zstd-19', '3.5x ratio') + ] + + for compression, expected_ratio in compression_tests: + # Select compression algorithm + compression_button = zfs_page.find_element(By.CSS_SELECTOR, '[name="compression"] + .select-lite__button') + compression_button.click() + + # Find and click the specific compression option + if compression == 'off': + xpath = "//div[contains(@class, 'select-lite__option') and contains(text(), 'Off')]" + elif compression == 'lz4': + xpath = "//div[contains(@class, 'select-lite__option') and contains(text(), 'LZ4')]" + elif compression == 'gzip': + xpath = "//div[contains(@class, 'select-lite__option') and contains(text(), 'Gzip (Balanced)')]" + elif compression == 'gzip-1': + xpath = "//div[contains(@class, 'select-lite__option') and contains(text(), 'Gzip-1')]" + elif compression == 'gzip-9': + xpath = "//div[contains(@class, 'select-lite__option') and contains(text(), 'Gzip-9')]" + elif compression == 'zstd': + xpath = "//div[contains(@class, 'select-lite__option') and contains(text(), 'Zstd (Modern)')]" + elif compression == 'zstd-1': + xpath = "//div[contains(@class, 'select-lite__option') and contains(text(), 'Zstd-1')]" + elif compression == 'zstd-19': + xpath = "//div[contains(@class, 'select-lite__option') and contains(text(), 'Zstd-19')]" + + compression_option = WebDriverWait(zfs_page, 10).until( + EC.element_to_be_clickable((By.XPATH, xpath)) + ) + compression_option.click() + + time.sleep(1) + + result = _get_zfs_result(zfs_page) + result_text = result.text + + # Verify compression ratio is shown + assert expected_ratio in result_text, f"Compression {compression} should show ratio: {expected_ratio}" + +def test_zfs_block_sizes(zfs_page): + """Test all block sizes are properly displayed""" + block_sizes = ['4K', '8K', '16K', '32K', '64K', '128K', '256K', '512K', '1M'] + + for block_size in block_sizes: + # Select block size + block_size_button = zfs_page.find_element(By.CSS_SELECTOR, '[name="blockSize"] + .select-lite__button') + block_size_button.click() + + # Find and click the specific block size option + block_option = WebDriverWait(zfs_page, 10).until( + EC.element_to_be_clickable((By.XPATH, f"//div[contains(@class, 'select-lite__option') and contains(text(), '{block_size}')]")) + ) + block_option.click() + + time.sleep(1) + + result = _get_zfs_result(zfs_page) + result_text = result.text + + # Verify block size is shown in results + assert block_size in result_text, f"Block size {block_size} should be displayed in results" + +def test_zfs_ashift_values(zfs_page): + """Test all ashift values and their byte calculations""" + ashift_tests = [ + (9, '512 bytes'), + (12, '4096 bytes'), + (13, '8192 bytes'), + (14, '16384 bytes') + ] + + for ashift, expected_bytes in ashift_tests: + # Select ashift value + ashift_button = zfs_page.find_element(By.CSS_SELECTOR, '[name="ashift"] + .select-lite__button') + ashift_button.click() + + # Find and click the specific ashift option + if ashift == 9: + xpath = "//div[contains(@class, 'select-lite__option') and contains(text(), '9 (512B sectors)')]" + elif ashift == 12: + xpath = "//div[contains(@class, 'select-lite__option') and contains(text(), '12 (4KB sectors)')]" + elif ashift == 13: + xpath = "//div[contains(@class, 'select-lite__option') and contains(text(), '13 (8KB sectors)')]" + elif ashift == 14: + xpath = "//div[contains(@class, 'select-lite__option') and contains(text(), '14 (16KB sectors)')]" + + ashift_option = WebDriverWait(zfs_page, 10).until( + EC.element_to_be_clickable((By.XPATH, xpath)) + ) + ashift_option.click() + + time.sleep(1) + + result = _get_zfs_result(zfs_page) + result_text = result.text + + # Verify ashift and byte calculation are shown + assert f"{ashift} ({expected_bytes})" in result_text, f"Ashift {ashift} should show {expected_bytes}" + +def test_zfs_arc_max_units(zfs_page): + """Test ARC max size with MB unit""" + # Test MB unit (default) + arc_max_input = zfs_page.find_element(By.NAME, "arcMax") + arc_max_input.clear() + arc_max_input.send_keys("8192") + + time.sleep(1) + + result = _get_zfs_result(zfs_page) + result_text = result.text + + assert "8192 MB" in result_text, "ARC max should show MB unit" + + # Test different MB value + arc_max_input.clear() + arc_max_input.send_keys("16384") + + time.sleep(1) + + result = _get_zfs_result(zfs_page) + result_text = result.text + + assert "16384 MB" in result_text, "ARC max should update when changed" + +def test_zfs_disk_count_validation_all_types(zfs_page): + """Test disk count validation for all pool types""" + validation_tests = [ + ('stripe', 0, False), # stripe can have 1+ disks + ('stripe', 1, True), # stripe minimum + ('mirror', 1, False), # mirror needs 2+ disks + ('mirror', 2, True), # mirror minimum + ('raidz1', 2, False), # raidz1 needs 3+ disks + ('raidz1', 3, True), # raidz1 minimum + ('raidz2', 3, False), # raidz2 needs 4+ disks + ('raidz2', 4, True), # raidz2 minimum + ('raidz3', 4, False), # raidz3 needs 5+ disks + ('raidz3', 5, True), # raidz3 minimum + ('mirror2x2', 3, False), # mirror2x2 needs 4 disks + ('mirror2x2', 4, True), # mirror2x2 minimum + ('mirror3x2', 5, False), # mirror3x2 needs 6 disks + ('mirror3x2', 6, True), # mirror3x2 minimum + ('raidz2x2', 7, False), # raidz2x2 needs 8+ disks + ('raidz2x2', 8, True), # raidz2x2 minimum + ] + + for pool_type, disk_count, should_be_valid in validation_tests: + # Select pool type + pool_type_button = zfs_page.find_element(By.CSS_SELECTOR, '[name="poolType"] + .select-lite__button') + pool_type_button.click() + + # Find and click the specific pool type option + if pool_type == 'stripe': + xpath = "//div[contains(@class, 'select-lite__option') and contains(text(), 'Stripe')]" + elif pool_type == 'mirror': + xpath = "//div[contains(@class, 'select-lite__option') and contains(text(), 'Mirror (2-way)')]" + elif pool_type == 'raidz1': + xpath = "//div[contains(@class, 'select-lite__option') and contains(text(), 'RAID-Z1')]" + elif pool_type == 'raidz2': + xpath = "//div[contains(@class, 'select-lite__option') and contains(text(), 'RAID-Z2')]" + elif pool_type == 'raidz3': + xpath = "//div[contains(@class, 'select-lite__option') and contains(text(), 'RAID-Z3')]" + elif pool_type == 'mirror2x2': + xpath = "//div[contains(@class, 'select-lite__option') and contains(text(), 'Mirror 2x2')]" + elif pool_type == 'mirror3x2': + xpath = "//div[contains(@class, 'select-lite__option') and contains(text(), 'Mirror 3x2')]" + elif pool_type == 'raidz2x2': + xpath = "//div[contains(@class, 'select-lite__option') and contains(text(), 'RAID-Z2 2x2')]" + + pool_option = WebDriverWait(zfs_page, 10).until( + EC.element_to_be_clickable((By.XPATH, xpath)) + ) + pool_option.click() + + # Set disk count + disk_count_input = zfs_page.find_element(By.NAME, "diskCount") + disk_count_input.clear() + disk_count_input.send_keys(str(disk_count)) + + time.sleep(1) + + result = _get_zfs_result(zfs_page) + result_text = result.text + + if should_be_valid: + assert "Error" not in result_text, f"Pool type {pool_type} with {disk_count} disks should be valid" + else: + assert "Error" in result_text, f"Pool type {pool_type} with {disk_count} disks should show error" + +def test_zfs_performance_calculations_accuracy(zfs_page): + """Test that performance calculations are mathematically correct""" + # Set up a known configuration + disk_count_input = zfs_page.find_element(By.NAME, "diskCount") + disk_count_input.clear() + disk_count_input.send_keys("6") + + time.sleep(1) + + result = _get_zfs_result(zfs_page) + result_text = result.text + + # Extract and verify IOPS calculation (should be 6 * 100 = 600) + assert "600" in result_text, "IOPS should be 600 for 6 disks (6 * 100)" + + # Extract and verify throughput calculation (should be 6 * 150 = 900) + assert "900 MB/s" in result_text, "Throughput should be 900 MB/s for 6 disks (6 * 150)" + + # Verify RAM recommendation (should be 6 * 4TB / 1024 = ~24GB minimum) + assert "24 GB minimum" in result_text, "RAM recommendation should be ~24GB for 24TB storage" + + + +def test_zfs_edge_cases_and_validation(zfs_page): + """Test edge cases and input validation""" + # Test negative disk count + disk_count_input = zfs_page.find_element(By.NAME, "diskCount") + disk_count_input.clear() + disk_count_input.send_keys("-1") + + time.sleep(1) + + result = _get_zfs_result(zfs_page) + result_text = result.text + + # Should handle negative gracefully (likely clamp to 1 or show error) + assert "Error" in result_text or "1" in result_text, "Should handle negative disk count" + + # Test zero disk count + disk_count_input.clear() + disk_count_input.send_keys("0") + + time.sleep(1) + + result = _get_zfs_result(zfs_page) + result_text = result.text + + assert "Error" in result_text, "Should show error for zero disk count" + + # Test very large disk count + disk_count_input.clear() + disk_count_input.send_keys("100") + + time.sleep(1) + + result = _get_zfs_result(zfs_page) + result_text = result.text + + # Should handle large numbers gracefully + assert "Error" not in result_text, "Should handle large disk count gracefully" + + # Test negative disk size + disk_size_input = zfs_page.find_element(By.NAME, "diskSize") + disk_size_input.clear() + disk_size_input.send_keys("-1") + + time.sleep(1) + + result = _get_zfs_result(zfs_page) + result_text = result.text + + # Should handle negative disk size gracefully + assert "Error" in result_text or "0" in result_text, "Should handle negative disk size" + +def test_zfs_state_persistence(zfs_page): + """Test that calculator state persists across page reloads""" + # Change some settings + disk_count_input = zfs_page.find_element(By.NAME, "diskCount") + disk_count_input.clear() + disk_count_input.send_keys("8") + + disk_size_input = zfs_page.find_element(By.NAME, "diskSize") + disk_size_input.clear() + disk_size_input.send_keys("2") + + # Change to TB + disk_size_unit_button = zfs_page.find_element(By.CSS_SELECTOR, '[name="diskSizeUnit"] + .select-lite__button') + disk_size_unit_button.click() + + tb_option = WebDriverWait(zfs_page, 10).until( + EC.element_to_be_clickable((By.XPATH, "//div[contains(@class, 'select-lite__option') and contains(text(), 'TB')]")) + ) + tb_option.click() + + time.sleep(1) + + # Reload the page + zfs_page.refresh() + + # Wait for page to reload + WebDriverWait(zfs_page, 10).until( + EC.presence_of_element_located((By.NAME, "poolType")) + ) + + time.sleep(2) + + # Check if settings were restored + disk_count_input = zfs_page.find_element(By.NAME, "diskCount") + disk_size_input = zfs_page.find_element(By.NAME, "diskSize") + + # Values should be restored (exact values may vary due to browser behavior) + assert disk_count_input.get_attribute('value') in ['8', '6'], "Disk count should be restored" + assert disk_size_input.get_attribute('value') in ['2', '4'], "Disk size should be restored" + +def test_zfs_compression_savings_calculation(zfs_page): + """Test ZFS-specific compression savings calculation""" + # Set up RAID-Z2 with 6 disks, 4TB each + pool_type_button = zfs_page.find_element(By.CSS_SELECTOR, '[name="poolType"] + .select-lite__button') + pool_type_button.click() + + raidz2_option = WebDriverWait(zfs_page, 10).until( + EC.element_to_be_clickable((By.XPATH, "//div[contains(@class, 'select-lite__option') and contains(text(), 'RAID-Z2')]")) + ) + raidz2_option.click() + + disk_count_input = zfs_page.find_element(By.NAME, "diskCount") + disk_count_input.clear() + disk_count_input.send_keys("6") + + disk_size_input = zfs_page.find_element(By.NAME, "diskSize") + disk_size_input.clear() + disk_size_input.send_keys("4") + + # Change to TB + disk_size_unit_button = zfs_page.find_element(By.CSS_SELECTOR, '[name="diskSizeUnit"] + .select-lite__button') + disk_size_unit_button.click() + + tb_option = WebDriverWait(zfs_page, 10).until( + EC.element_to_be_clickable((By.XPATH, "//div[contains(@class, 'select-lite__option') and contains(text(), 'TB')]")) + ) + tb_option.click() + + # Test LZ4 compression (2.1x ratio) + compression_button = zfs_page.find_element(By.CSS_SELECTOR, '[name="compression"] + .select-lite__button') + compression_button.click() + + lz4_option = WebDriverWait(zfs_page, 10).until( + EC.element_to_be_clickable((By.XPATH, "//div[contains(@class, 'select-lite__option') and contains(text(), 'LZ4')]")) + ) + lz4_option.click() + + time.sleep(1) + + result = _get_zfs_result(zfs_page) + result_text = result.text + + # RAID-Z2 with 6 disks: (6-2) * 4TB = 16TB usable + # With LZ4 (2.1x): 16TB * 2.1 = 33.6TB effective + # Compression savings: (33.6 - 16) / 16 * 100 = 110% + assert "16.00 TB" in result_text, "Usable capacity should be 16TB" + assert "33.60 TB" in result_text, "Effective capacity should be 33.6TB with LZ4" + assert "110.0%" in result_text, "Compression savings should be 110%" + +def test_zfs_efficiency_calculation(zfs_page): + """Test ZFS-specific efficiency calculation""" + # Test different pool types for efficiency + efficiency_tests = [ + ('stripe', 1, '100.0%'), # 100% efficiency + ('mirror', 2, '50.0%'), # 50% efficiency + ('raidz1', 3, '66.7%'), # 66.7% efficiency + ('raidz2', 6, '66.7%'), # 66.7% efficiency + ] + + for pool_type, disk_count, expected_efficiency in efficiency_tests: + # Select pool type + pool_type_button = zfs_page.find_element(By.CSS_SELECTOR, '[name="poolType"] + .select-lite__button') + pool_type_button.click() + + if pool_type == 'stripe': + xpath = "//div[contains(@class, 'select-lite__option') and contains(text(), 'Stripe')]" + elif pool_type == 'mirror': + xpath = "//div[contains(@class, 'select-lite__option') and contains(text(), 'Mirror (2-way)')]" + elif pool_type == 'raidz1': + xpath = "//div[contains(@class, 'select-lite__option') and contains(text(), 'RAID-Z1')]" + elif pool_type == 'raidz2': + xpath = "//div[contains(@class, 'select-lite__option') and contains(text(), 'RAID-Z2')]" + + pool_option = WebDriverWait(zfs_page, 10).until( + EC.element_to_be_clickable((By.XPATH, xpath)) + ) + pool_option.click() + + # Set disk count + disk_count_input = zfs_page.find_element(By.NAME, "diskCount") + disk_count_input.clear() + disk_count_input.send_keys(str(disk_count)) + + time.sleep(1) + + result = _get_zfs_result(zfs_page) + result_text = result.text + + assert expected_efficiency in result_text, f"Pool type {pool_type} with {disk_count} disks should have {expected_efficiency} efficiency" + +def test_zfs_vdev_count_logic(zfs_page): + """Test ZFS-specific vdev count calculations""" + vdev_tests = [ + ('stripe', 1, '1 vdev'), + ('mirror', 2, '1 vdev'), + ('mirror', 4, '2 vdevs'), + ('raidz1', 3, '1 vdev'), + ('raidz2', 6, '1 vdev'), + ('mirror2x2', 4, '2 vdevs'), + ('mirror3x2', 6, '3 vdevs'), + ('raidz2x2', 8, '2 vdevs'), + ] + + for pool_type, disk_count, expected_vdevs in vdev_tests: + # Select pool type + pool_type_button = zfs_page.find_element(By.CSS_SELECTOR, '[name="poolType"] + .select-lite__button') + pool_type_button.click() + + if pool_type == 'stripe': + xpath = "//div[contains(@class, 'select-lite__option') and contains(text(), 'Stripe')]" + elif pool_type == 'mirror': + xpath = "//div[contains(@class, 'select-lite__option') and contains(text(), 'Mirror (2-way)')]" + elif pool_type == 'raidz1': + xpath = "//div[contains(@class, 'select-lite__option') and contains(text(), 'RAID-Z1')]" + elif pool_type == 'raidz2': + xpath = "//div[contains(@class, 'select-lite__option') and contains(text(), 'RAID-Z2')]" + elif pool_type == 'mirror2x2': + xpath = "//div[contains(@class, 'select-lite__option') and contains(text(), 'Mirror 2x2')]" + elif pool_type == 'mirror3x2': + xpath = "//div[contains(@class, 'select-lite__option') and contains(text(), 'Mirror 3x2')]" + elif pool_type == 'raidz2x2': + xpath = "//div[contains(@class, 'select-lite__option') and contains(text(), 'RAID-Z2 2x2')]" + + pool_option = WebDriverWait(zfs_page, 10).until( + EC.element_to_be_clickable((By.XPATH, xpath)) + ) + pool_option.click() + + # Set disk count + disk_count_input = zfs_page.find_element(By.NAME, "diskCount") + disk_count_input.clear() + disk_count_input.send_keys(str(disk_count)) + + time.sleep(1) + + result = _get_zfs_result(zfs_page) + result_text = result.text + + assert expected_vdevs in result_text, f"Pool type {pool_type} with {disk_count} disks should have {expected_vdevs}" + +def test_zfs_block_size_parsing(zfs_page): + """Test ZFS-specific block size parsing and display""" + block_size_tests = [ + ('4K', '4K'), + ('128K', '128K'), + ('1M', '1M'), + ] + + for block_size, expected_display in block_size_tests: + # Select block size + block_size_button = zfs_page.find_element(By.CSS_SELECTOR, '[name="blockSize"] + .select-lite__button') + block_size_button.click() + + block_option = WebDriverWait(zfs_page, 10).until( + EC.element_to_be_clickable((By.XPATH, f"//div[contains(@class, 'select-lite__option') and contains(text(), '{block_size}')]")) + ) + block_option.click() + + time.sleep(1) + + result = _get_zfs_result(zfs_page) + result_text = result.text + + assert f"Block Size: {expected_display}" in result_text, f"Block size {block_size} should display as {expected_display}" + +def test_zfs_ashift_byte_calculation(zfs_page): + """Test ZFS-specific ashift to byte conversion""" + ashift_byte_tests = [ + (9, '512 bytes'), + (12, '4096 bytes'), + (13, '8192 bytes'), + (14, '16384 bytes') + ] + + for ashift, expected_bytes in ashift_byte_tests: + # Select ashift value + ashift_button = zfs_page.find_element(By.CSS_SELECTOR, '[name="ashift"] + .select-lite__button') + ashift_button.click() + + if ashift == 9: + xpath = "//div[contains(@class, 'select-lite__option') and contains(text(), '9 (512B sectors)')]" + elif ashift == 12: + xpath = "//div[contains(@class, 'select-lite__option') and contains(text(), '12 (4KB sectors)')]" + elif ashift == 13: + xpath = "//div[contains(@class, 'select-lite__option') and contains(text(), '13 (8KB sectors)')]" + elif ashift == 14: + xpath = "//div[contains(@class, 'select-lite__option') and contains(text(), '14 (16KB sectors)')]" + + ashift_option = WebDriverWait(zfs_page, 10).until( + EC.element_to_be_clickable((By.XPATH, xpath)) + ) + ashift_option.click() + + time.sleep(1) + + result = _get_zfs_result(zfs_page) + result_text = result.text + + # Verify the mathematical calculation: 2^ashift = expected_bytes + expected_calculation = 2 ** ashift + assert f"{ashift} ({expected_calculation} bytes)" in result_text, f"Ashift {ashift} should calculate to {expected_calculation} bytes" + +def test_zfs_raidz2x2_complex_calculation(zfs_page): + """Test the complex RAID-Z2 2x2 calculation: 2 * (diskCount / 2 - 2) * diskSizeGB""" + # Select RAID-Z2 2x2 + pool_type_button = zfs_page.find_element(By.CSS_SELECTOR, '[name="poolType"] + .select-lite__button') + pool_type_button.click() + + raidz2x2_option = WebDriverWait(zfs_page, 10).until( + EC.element_to_be_clickable((By.XPATH, "//div[contains(@class, 'select-lite__option') and contains(text(), 'RAID-Z2 2x2')]")) + ) + raidz2x2_option.click() + + # Test with 8 disks, 2TB each + disk_count_input = zfs_page.find_element(By.NAME, "diskCount") + disk_count_input.clear() + disk_count_input.send_keys("8") + + disk_size_input = zfs_page.find_element(By.NAME, "diskSize") + disk_size_input.clear() + disk_size_input.send_keys("2") + + # Change to TB + disk_size_unit_button = zfs_page.find_element(By.CSS_SELECTOR, '[name="diskSizeUnit"] + .select-lite__button') + disk_size_unit_button.click() + + tb_option = WebDriverWait(zfs_page, 10).until( + EC.element_to_be_clickable((By.XPATH, "//div[contains(@class, 'select-lite__option') and contains(text(), 'TB')]")) + ) + tb_option.click() + + time.sleep(1) + + result = _get_zfs_result(zfs_page) + result_text = result.text + + # RAID-Z2 2x2 with 8 disks, 2TB each: + # Formula: 2 * (8/2 - 2) * 2TB = 2 * (4 - 2) * 2TB = 2 * 2 * 2TB = 8TB + assert "8.00 TB" in result_text, "RAID-Z2 2x2 with 8 disks of 2TB should have 8TB usable capacity" + +def test_zfs_mirror_vdev_count_calculation(zfs_page): + """Test ZFS-specific mirror vdev count: Math.floor(diskCount / 2)""" + # Test different mirror configurations + mirror_tests = [ + (2, '1 vdev'), # 2 disks = 1 mirror vdev + (4, '2 vdevs'), # 4 disks = 2 mirror vdevs + (6, '3 vdevs'), # 6 disks = 3 mirror vdevs + (8, '4 vdevs'), # 8 disks = 4 mirror vdevs + ] + + for disk_count, expected_vdevs in mirror_tests: + # Select mirror pool type + pool_type_button = zfs_page.find_element(By.CSS_SELECTOR, '[name="poolType"] + .select-lite__button') + pool_type_button.click() + + mirror_option = WebDriverWait(zfs_page, 10).until( + EC.element_to_be_clickable((By.XPATH, "//div[contains(@class, 'select-lite__option') and contains(text(), 'Mirror (2-way)')]")) + ) + mirror_option.click() + + # Set disk count + disk_count_input = zfs_page.find_element(By.NAME, "diskCount") + disk_count_input.clear() + disk_count_input.send_keys(str(disk_count)) + + time.sleep(1) + + result = _get_zfs_result(zfs_page) + result_text = result.text + + assert expected_vdevs in result_text, f"Mirror with {disk_count} disks should have {expected_vdevs}" + +def test_zfs_compression_ratio_edge_cases(zfs_page): + """Test ZFS compression ratio edge cases""" + # Test with compression off (1.0x ratio) + compression_button = zfs_page.find_element(By.CSS_SELECTOR, '[name="compression"] + .select-lite__button') + compression_button.click() + + off_option = WebDriverWait(zfs_page, 10).until( + EC.element_to_be_clickable((By.XPATH, "//div[contains(@class, 'select-lite__option') and contains(text(), 'Off')]")) + ) + off_option.click() + + time.sleep(1) + + result = _get_zfs_result(zfs_page) + result_text = result.text + + # With compression off, effective capacity should equal usable capacity + # Compression savings should be 0% + assert "1.0x ratio" in result_text, "Compression off should show 1.0x ratio" + assert "0.0%" in result_text, "Compression savings should be 0% with compression off"