const request = require('supertest'); const app = require('../server'); const database = require('../models/database'); const fs = require('fs'); const path = require('path'); const XLSX = require('xlsx'); describe('Performance Tests - Large Datasets', () => { let testDbPath; beforeAll(async () => { // Set up test database with performance optimizations testDbPath = path.join(__dirname, '..', 'test_performance.db'); if (fs.existsSync(testDbPath)) { fs.unlinkSync(testDbPath); } database.dbPath = testDbPath; await database.initialize(); // Apply additional performance settings for testing const db = database.getDatabase(); db.pragma('cache_size = 2000'); // Increase cache size for tests db.pragma('temp_store = memory'); db.pragma('mmap_size = 536870912'); // 512MB }); afterAll(async () => { database.close(); if (fs.existsSync(testDbPath)) { fs.unlinkSync(testDbPath); } }); beforeEach(async () => { // Clean up database before each test const db = database.getDatabase(); db.exec('DELETE FROM inventory_history'); db.exec('DELETE FROM inventory'); db.exec('DELETE FROM products'); db.exec('DELETE FROM import_sessions'); }); describe('Large Dataset Import Performance', () => { test('should import 1000+ products within acceptable time', async () => { const productCount = 1000; const testData = [['Product Code', 'Description', 'Quantity', 'Category', 'Unit of Measure']]; // Generate large dataset console.log(`Generating ${productCount} test products...`); for (let i = 1; i <= productCount; i++) { testData.push([ `PERF${i.toString().padStart(6, '0')}`, `Performance Test Product ${i} - ${Math.random().toString(36).substring(7)}`, Math.floor(Math.random() * 1000) + 1, ['Electronics', 'Books', 'Clothing', 'Home', 'Sports'][i % 5], ['pcs', 'kg', 'lbs', 'units', 'boxes'][i % 5] ]); } const worksheet = XLSX.utils.aoa_to_sheet(testData); const workbook = XLSX.utils.book_new(); XLSX.utils.book_append_sheet(workbook, worksheet, 'LargeDataset'); const buffer = XLSX.write(workbook, { type: 'buffer', bookType: 'xlsx' }); console.log(`Starting import of ${productCount} products...`); const startTime = Date.now(); const response = await request(app) .post('/api/products/import/excel') .attach('file', buffer, 'large-dataset.xlsx') .timeout(30000) // 30 second timeout .expect(200); const duration = Date.now() - startTime; console.log(`Import completed in ${duration}ms (${(duration/1000).toFixed(2)}s)`); console.log(`Average: ${(duration/productCount).toFixed(2)}ms per product`); expect(response.body.success).toBe(true); expect(response.body.data.importResults.imported).toBe(productCount); expect(duration).toBeLessThan(15000); // Should complete within 15 seconds expect(duration / productCount).toBeLessThan(15); // Less than 15ms per product }); test('should handle 5000+ products with memory efficiency', async () => { const productCount = 5000; console.log(`Testing memory efficiency with ${productCount} products...`); const initialMemory = process.memoryUsage(); console.log('Initial memory usage:', { rss: `${(initialMemory.rss / 1024 / 1024).toFixed(2)}MB`, heapUsed: `${(initialMemory.heapUsed / 1024 / 1024).toFixed(2)}MB` }); // Generate large dataset in chunks to test streaming const chunkSize = 1000; let totalImported = 0; for (let chunk = 0; chunk < Math.ceil(productCount / chunkSize); chunk++) { const chunkStart = chunk * chunkSize + 1; const chunkEnd = Math.min((chunk + 1) * chunkSize, productCount); const chunkData = [['Product Code', 'Description', 'Quantity', 'Category']]; for (let i = chunkStart; i <= chunkEnd; i++) { chunkData.push([ `CHUNK${chunk}_${i.toString().padStart(6, '0')}`, `Chunk ${chunk} Product ${i}`, Math.floor(Math.random() * 100) + 1, `Category${i % 10}` ]); } const worksheet = XLSX.utils.aoa_to_sheet(chunkData); const workbook = XLSX.utils.book_new(); XLSX.utils.book_append_sheet(workbook, worksheet, `Chunk${chunk}`); const buffer = XLSX.write(workbook, { type: 'buffer', bookType: 'xlsx' }); const chunkStartTime = Date.now(); const response = await request(app) .post('/api/products/import/excel') .attach('file', buffer, `chunk-${chunk}.xlsx`) .timeout(30000) .expect(200); const chunkDuration = Date.now() - chunkStartTime; totalImported += response.body.data.importResults.imported; console.log(`Chunk ${chunk + 1}/${Math.ceil(productCount / chunkSize)} completed in ${chunkDuration}ms`); // Check memory usage after each chunk const currentMemory = process.memoryUsage(); const memoryIncrease = (currentMemory.heapUsed - initialMemory.heapUsed) / 1024 / 1024; console.log(`Memory increase: ${memoryIncrease.toFixed(2)}MB`); // Memory should not increase excessively (less than 100MB per 1000 products) expect(memoryIncrease).toBeLessThan(100 * (chunk + 1)); } expect(totalImported).toBe(productCount); console.log(`Total imported: ${totalImported} products`); // Verify final memory usage is reasonable const finalMemory = process.memoryUsage(); const totalMemoryIncrease = (finalMemory.heapUsed - initialMemory.heapUsed) / 1024 / 1024; console.log(`Total memory increase: ${totalMemoryIncrease.toFixed(2)}MB`); expect(totalMemoryIncrease).toBeLessThan(500); // Less than 500MB total increase }); }); describe('Database Query Performance', () => { beforeEach(async () => { // Create a large dataset for query testing const db = database.getDatabase(); const insertProduct = db.prepare(` INSERT INTO products (product_code, description, category, unit_of_measure) VALUES (?, ?, ?, ?) `); const insertInventory = db.prepare(` INSERT INTO inventory (product_id, current_level, minimum_level, maximum_level) VALUES (?, ?, ?, ?) `); console.log('Setting up large dataset for query performance tests...'); const transaction = db.transaction(() => { for (let i = 1; i <= 2000; i++) { const result = insertProduct.run( `QUERY${i.toString().padStart(6, '0')}`, `Query Test Product ${i}`, `Category${i % 20}`, 'pcs' ); insertInventory.run( result.lastInsertRowid, Math.floor(Math.random() * 1000) + 1, 10, 500 ); } }); transaction(); console.log('Large dataset setup completed'); }); test('should perform fast product lookups with indexes', async () => { const iterations = 100; const startTime = Date.now(); for (let i = 0; i < iterations; i++) { const productCode = `QUERY${Math.floor(Math.random() * 2000 + 1).toString().padStart(6, '0')}`; const response = await request(app) .get(`/api/products/barcode/${productCode}`) .expect(200); expect(response.body.success).toBe(true); } const duration = Date.now() - startTime; const avgTime = duration / iterations; console.log(`${iterations} product lookups completed in ${duration}ms`); console.log(`Average lookup time: ${avgTime.toFixed(2)}ms`); expect(avgTime).toBeLessThan(50); // Less than 50ms per lookup }); test('should perform fast inventory queries with pagination', async () => { const pageSize = 50; const totalPages = 10; const startTime = Date.now(); for (let page = 0; page < totalPages; page++) { const response = await request(app) .get(`/api/inventory?limit=${pageSize}&offset=${page * pageSize}`) .expect(200); expect(response.body.success).toBe(true); expect(response.body.data.length).toBeLessThanOrEqual(pageSize); } const duration = Date.now() - startTime; const avgTime = duration / totalPages; console.log(`${totalPages} paginated queries completed in ${duration}ms`); console.log(`Average query time: ${avgTime.toFixed(2)}ms`); expect(avgTime).toBeLessThan(100); // Less than 100ms per paginated query }); test('should perform fast filtered searches', async () => { const categories = ['Category0', 'Category5', 'Category10', 'Category15']; const startTime = Date.now(); for (const category of categories) { const response = await request(app) .get(`/api/inventory?category=${category}`) .expect(200); expect(response.body.success).toBe(true); expect(response.body.data.length).toBeGreaterThan(0); // Verify all results match the filter response.body.data.forEach(item => { expect(item.category).toBe(category); }); } const duration = Date.now() - startTime; const avgTime = duration / categories.length; console.log(`${categories.length} filtered searches completed in ${duration}ms`); console.log(`Average search time: ${avgTime.toFixed(2)}ms`); expect(avgTime).toBeLessThan(200); // Less than 200ms per filtered search }); test('should handle complex aggregation queries efficiently', async () => { const startTime = Date.now(); // Test low stock query const lowStockResponse = await request(app) .get('/api/inventory/low-stock') .expect(200); // Test inventory summary with grouping const summaryResponse = await request(app) .get('/api/inventory?groupBy=category') .expect(200); const duration = Date.now() - startTime; console.log(`Complex aggregation queries completed in ${duration}ms`); expect(duration).toBeLessThan(1000); // Less than 1 second for complex queries expect(lowStockResponse.body.success).toBe(true); expect(summaryResponse.body.success).toBe(true); }); }); describe('Concurrent Operations Performance', () => { beforeEach(async () => { // Create products for concurrent testing const db = database.getDatabase(); const insertProduct = db.prepare(` INSERT INTO products (product_code, description, category) VALUES (?, ?, ?) `); const insertInventory = db.prepare(` INSERT INTO inventory (product_id, current_level, minimum_level, maximum_level) VALUES (?, ?, ?, ?) `); const transaction = db.transaction(() => { for (let i = 1; i <= 100; i++) { const result = insertProduct.run( `CONCURRENT${i.toString().padStart(3, '0')}`, `Concurrent Test Product ${i}`, 'Test' ); insertInventory.run(result.lastInsertRowid, 100, 10, 200); } }); transaction(); }); test('should handle concurrent inventory updates efficiently', async () => { const concurrentUsers = 20; const updatesPerUser = 5; console.log(`Testing ${concurrentUsers} concurrent users with ${updatesPerUser} updates each...`); const startTime = Date.now(); const promises = []; for (let user = 0; user < concurrentUsers; user++) { for (let update = 0; update < updatesPerUser; update++) { const productId = Math.floor(Math.random() * 100) + 1; const newLevel = Math.floor(Math.random() * 200) + 1; const promise = request(app) .put(`/api/inventory/product/${productId}/level`) .send({ newLevel: newLevel, changeReason: `Concurrent update by user ${user}`, updatedBy: `user-${user}` }); promises.push(promise); } } const results = await Promise.allSettled(promises); const duration = Date.now() - startTime; const successful = results.filter(r => r.status === 'fulfilled' && r.value.status === 200).length; const conflicts = results.filter(r => r.status === 'fulfilled' && r.value.status === 409).length; const errors = results.filter(r => r.status === 'rejected' || (r.status === 'fulfilled' && r.value.status >= 500)).length; console.log(`Concurrent updates completed in ${duration}ms`); console.log(`Successful: ${successful}, Conflicts: ${conflicts}, Errors: ${errors}`); console.log(`Average time per update: ${(duration / promises.length).toFixed(2)}ms`); expect(successful + conflicts).toBe(promises.length); // All should either succeed or conflict expect(errors).toBe(0); // No server errors expect(duration).toBeLessThan(10000); // Complete within 10 seconds expect(successful).toBeGreaterThan(promises.length * 0.7); // At least 70% success rate }); test('should handle concurrent read operations efficiently', async () => { const concurrentReads = 50; console.log(`Testing ${concurrentReads} concurrent read operations...`); const startTime = Date.now(); const promises = []; for (let i = 0; i < concurrentReads; i++) { const operations = [ request(app).get('/api/products'), request(app).get('/api/inventory'), request(app).get('/api/inventory/low-stock'), request(app).get(`/api/products/${Math.floor(Math.random() * 100) + 1}`), request(app).get(`/api/inventory/product/${Math.floor(Math.random() * 100) + 1}`) ]; promises.push(...operations); } const results = await Promise.allSettled(promises); const duration = Date.now() - startTime; const successful = results.filter(r => r.status === 'fulfilled' && r.value.status === 200).length; const errors = results.filter(r => r.status === 'rejected' || (r.status === 'fulfilled' && r.value.status >= 400)).length; console.log(`${promises.length} concurrent reads completed in ${duration}ms`); console.log(`Successful: ${successful}, Errors: ${errors}`); console.log(`Average time per read: ${(duration / promises.length).toFixed(2)}ms`); expect(successful).toBe(promises.length); // All reads should succeed expect(errors).toBe(0); expect(duration).toBeLessThan(5000); // Complete within 5 seconds expect(duration / promises.length).toBeLessThan(100); // Less than 100ms per read }); }); describe('Export Performance', () => { beforeEach(async () => { // Create large dataset for export testing const db = database.getDatabase(); const insertProduct = db.prepare(` INSERT INTO products (product_code, description, category, unit_of_measure) VALUES (?, ?, ?, ?) `); const insertInventory = db.prepare(` INSERT INTO inventory (product_id, current_level, minimum_level, maximum_level, last_updated, updated_by) VALUES (?, ?, ?, ?, datetime('now'), ?) `); console.log('Setting up large dataset for export performance tests...'); const transaction = db.transaction(() => { for (let i = 1; i <= 1500; i++) { const result = insertProduct.run( `EXPORT${i.toString().padStart(6, '0')}`, `Export Test Product ${i} - ${Math.random().toString(36).substring(7)}`, `Category${i % 15}`, ['pcs', 'kg', 'lbs', 'units'][i % 4] ); insertInventory.run( result.lastInsertRowid, Math.floor(Math.random() * 500) + 1, Math.floor(Math.random() * 20) + 5, Math.floor(Math.random() * 200) + 300, `export-user-${i % 10}` ); } }); transaction(); console.log('Export dataset setup completed'); }); test('should export large datasets efficiently', async () => { console.log('Testing large dataset export performance...'); const startTime = Date.now(); const response = await request(app) .get('/api/codes/export/excel') .timeout(15000) // 15 second timeout .expect(200); const duration = Date.now() - startTime; console.log(`Export of 1500 products completed in ${duration}ms (${(duration/1000).toFixed(2)}s)`); expect(response.headers['content-type']).toContain('application/vnd.openxmlformats'); expect(duration).toBeLessThan(10000); // Should complete within 10 seconds // Verify export content const workbook = XLSX.read(response.body, { type: 'buffer' }); const sheet = workbook.Sheets[workbook.SheetNames[0]]; const data = XLSX.utils.sheet_to_json(sheet); expect(data.length).toBe(1500); expect(data[0]).toHaveProperty('Product Code'); expect(data[0]).toHaveProperty('Description'); expect(data[0]).toHaveProperty('Current Level'); }); test('should handle filtered exports efficiently', async () => { const filters = [ { category: 'Category0' }, { category: 'Category5' }, { lowStock: 'true' }, { category: 'Category10', minLevel: 50 } ]; for (const filter of filters) { const queryString = new URLSearchParams(filter).toString(); const startTime = Date.now(); const response = await request(app) .get(`/api/codes/export/excel?${queryString}`) .timeout(10000) .expect(200); const duration = Date.now() - startTime; console.log(`Filtered export (${queryString}) completed in ${duration}ms`); expect(duration).toBeLessThan(5000); // Filtered exports should be faster expect(response.headers['content-type']).toContain('application/vnd.openxmlformats'); } }); }); describe('Memory and Resource Management', () => { test('should maintain stable memory usage during sustained operations', async () => { const initialMemory = process.memoryUsage(); console.log('Initial memory:', { rss: `${(initialMemory.rss / 1024 / 1024).toFixed(2)}MB`, heapUsed: `${(initialMemory.heapUsed / 1024 / 1024).toFixed(2)}MB` }); // Perform sustained operations for (let cycle = 0; cycle < 10; cycle++) { // Create some products const products = []; for (let i = 0; i < 50; i++) { const response = await request(app) .post('/api/products') .send({ name: `MEMORY${cycle}_${i}`, description: `Memory Test Product ${cycle}-${i}`, category: 'MemoryTest' }) .expect(201); products.push(response.body.data.id); } // Update inventory levels for (const productId of products) { await request(app) .post(`/api/inventory/product/${productId}`) .send({ initialLevel: Math.floor(Math.random() * 100) + 1, minimumLevel: 5, maximumLevel: 150, updatedBy: 'memory-test' }) .expect(201); } // Clean up products to test garbage collection for (const productId of products) { await request(app) .delete(`/api/products/${productId}`) .expect(200); } // Check memory usage const currentMemory = process.memoryUsage(); const memoryIncrease = (currentMemory.heapUsed - initialMemory.heapUsed) / 1024 / 1024; console.log(`Cycle ${cycle + 1}/10 - Memory increase: ${memoryIncrease.toFixed(2)}MB`); // Memory should not continuously increase expect(memoryIncrease).toBeLessThan(50 * (cycle + 1)); // Allow some growth but not excessive } // Force garbage collection if available if (global.gc) { global.gc(); } const finalMemory = process.memoryUsage(); const totalIncrease = (finalMemory.heapUsed - initialMemory.heapUsed) / 1024 / 1024; console.log('Final memory increase:', `${totalIncrease.toFixed(2)}MB`); expect(totalIncrease).toBeLessThan(100); // Total increase should be reasonable }); }); });