# test-robustness.R # Pruebas de robustez, concurrencia y estabilidad del paquete # ============================================================================== # PRUEBAS DE ROBUSTEZ DE CONEXION SQLite # ============================================================================== test_that("multiples conexiones consecutivas funcionan", { skip_on_cran() # Abrir y cerrar multiples conexiones seguidas for (i in 1:10) { resultado <- cie_lookup("E11.0") expect_equal(nrow(resultado), 1) } }) test_that("multiples queries consecutivas funcionan", { skip_on_cran() # Ejecutar multiples queries seguidas queries <- c( "SELECT COUNT(*) as n FROM cie10", "SELECT * FROM cie10 WHERE codigo = 'E11.0'", "SELECT * FROM cie10 WHERE codigo LIKE 'E11%' LIMIT 5", "SELECT capitulo, COUNT(*) as n FROM cie10 GROUP BY capitulo" ) for (query in queries) { resultado <- cie10_sql(query) expect_s3_class(resultado, "tbl_df") } }) test_that("funciones diferentes pueden ejecutarse intercaladas", { skip_on_cran() # Intercalar diferentes funciones resultado1 <- cie_lookup("E11.0") resultado2 <- cie_search("diabetes", threshold = 0.70, max_results = 5) resultado3 <- cie10_sql("SELECT COUNT(*) as n FROM cie10") resultado4 <- cie_lookup("Z00") resultado5 <- cie_expand("E11") expect_equal(nrow(resultado1), 1) expect_gt(nrow(resultado2), 0) expect_gt(resultado3$n, 5000) expect_s3_class(resultado4, "tbl_df") expect_true(length(resultado5) > 0) }) # ============================================================================== # PRUEBAS DE MEMORIA Y RECURSOS # ============================================================================== test_that("queries grandes no causan problemas de memoria", { skip_on_cran() # Query que retorna muchos resultados resultado <- cie10_sql("SELECT * FROM cie10 LIMIT 5000") expect_s3_class(resultado, "tbl_df") expect_lte(nrow(resultado), 5000) # Liberar memoria rm(resultado) gc() }) test_that("cie_search con muchos resultados funciona", { skip_on_cran() # Busqueda con threshold bajo que retorne muchos resultados # Nota: texto debe tener al menos 3 caracteres resultado <- cie_search("diabetes", threshold = 0.01, max_results = 1000) expect_s3_class(resultado, "tbl_df") expect_lte(nrow(resultado), 1000) }) test_that("cie_lookup con vector grande funciona", { skip_on_cran() # Vector de codigos grande codigos <- c( paste0("E11.", 0:9), paste0("E10.", 0:9), paste0("I10.", 0:9), paste0("Z00.", 0:9) ) suppressMessages({ resultado <- cie_lookup(codigos) }) expect_s3_class(resultado, "tbl_df") }) # ============================================================================== # PRUEBAS DE CACHE Y ESTADO # ============================================================================== test_that("cie10_clear_cache funciona sin base de datos", { skip_on_cran() # No debe dar error si la cache no existe # (puede que ya exista, pero el mensaje debe ser apropiado) expect_no_error({ suppressMessages(cie10_clear_cache()) }) }) test_that("base de datos se reconstruye despues de clear_cache", { skip_on_cran() # Limpiar cache suppressMessages(cie10_clear_cache()) # La siguiente query debe reconstruir la base expect_message({ resultado <- cie_lookup("E11.0") }, "Inicializada SQLite DB") expect_equal(nrow(resultado), 1) }) test_that("base de datos persiste entre llamadas", { skip_on_cran() # Primera llamada resultado1 <- cie_lookup("E11.0") # Segunda llamada no debe mostrar mensaje de inicializacion expect_silent({ resultado2 <- cie_lookup("E11.0") }) expect_equal(resultado1$codigo, resultado2$codigo) }) # ============================================================================== # PRUEBAS DE INTEGRIDAD DE DATOS # ============================================================================== test_that("todos los codigos tienen descripcion", { skip_on_cran() resultado <- cie10_sql("SELECT COUNT(*) as n FROM cie10 WHERE descripcion IS NULL OR descripcion = ''") expect_equal(resultado$n, 0, info = "Todos los codigos deben tener descripcion") }) test_that("todos los codigos son unicos", { skip_on_cran() resultado_total <- cie10_sql("SELECT COUNT(*) as n FROM cie10") resultado_unique <- cie10_sql("SELECT COUNT(DISTINCT codigo) as n FROM cie10") expect_equal(resultado_total$n, resultado_unique$n) }) test_that("codigos principales siguen formato CIE-10 valido", { skip_on_cran() # Obtener codigos principales (excluyendo morfologia M80xx/x que tienen formato especial) codigos <- cie10_sql("SELECT codigo FROM cie10 WHERE codigo NOT LIKE 'M80%/%'")$codigo # Validar formato validos <- cie_validate_vector(codigos) # La mayoria deben ser validos (permitir algunos especiales) porcentaje_validos <- sum(validos) / length(validos) expect_gt(porcentaje_validos, 0.95) }) test_that("capitulos estan correctamente asignados", { skip_on_cran() # Verificar que los capitulos existen y son consistentes resultado <- cie10_sql("SELECT DISTINCT capitulo FROM cie10 WHERE capitulo IS NOT NULL") expect_gt(nrow(resultado), 0) }) # ============================================================================== # PRUEBAS DE RECUPERACION DE ERRORES # ============================================================================== test_that("error en query no corrompe estado", { skip_on_cran() # Query invalida debe dar error expect_error(cie10_sql("DROP TABLE cie10")) # Pero la siguiente query valida debe funcionar resultado <- cie10_sql("SELECT COUNT(*) as n FROM cie10") expect_gt(resultado$n, 5000) }) test_that("codigo invalido no afecta siguientes busquedas", { skip_on_cran() # Buscar codigo invalido suppressMessages({ resultado_invalido <- cie_lookup("XXXXXXXXX") }) expect_equal(nrow(resultado_invalido), 0) # Buscar codigo valido despues resultado_valido <- cie_lookup("E11.0") expect_equal(nrow(resultado_valido), 1) }) test_that("texto invalido en search no afecta siguientes busquedas", { skip_on_cran() # Busqueda que no encuentra nada suppressMessages({ resultado_vacio <- cie_search("xyzxyzxyzxyz", threshold = 0.99) }) # Busqueda normal debe funcionar resultado_normal <- cie_search("diabetes", threshold = 0.70) expect_gt(nrow(resultado_normal), 0) }) # ============================================================================== # PRUEBAS DE TIPOS DE RETORNO # ============================================================================== test_that("cie_lookup siempre retorna tibble", { skip_on_cran() # Con resultado resultado1 <- cie_lookup("E11.0") expect_s3_class(resultado1, "tbl_df") # Sin resultado suppressMessages({ resultado2 <- cie_lookup("XXXXX") }) expect_s3_class(resultado2, "tbl_df") # Con vector resultado3 <- cie_lookup(c("E11.0", "Z00")) expect_s3_class(resultado3, "tbl_df") }) test_that("cie_search siempre retorna tibble", { skip_on_cran() # Con resultados resultado1 <- cie_search("diabetes", threshold = 0.70) expect_s3_class(resultado1, "tbl_df") # Sin resultados suppressMessages({ resultado2 <- cie_search("xyzxyzxyz", threshold = 0.99) }) expect_s3_class(resultado2, "tbl_df") }) test_that("cie10_sql siempre retorna tibble", { skip_on_cran() resultado <- cie10_sql("SELECT * FROM cie10 LIMIT 1") expect_s3_class(resultado, "tbl_df") }) test_that("cie_map_comorbid siempre retorna tibble", { resultado <- cie_map_comorbid(c("E11.0", "I50.9")) expect_s3_class(resultado, "tbl_df") resultado_vacio <- cie_map_comorbid(character(0)) expect_s3_class(resultado_vacio, "tbl_df") }) # ============================================================================== # PRUEBAS DE COLUMNAS ESPERADAS # ============================================================================== test_that("cie_lookup retorna columnas esperadas", { skip_on_cran() resultado <- cie_lookup("E11.0") columnas_esperadas <- c("codigo", "descripcion", "categoria") expect_true(all(columnas_esperadas %in% names(resultado))) }) test_that("cie_search retorna columnas esperadas", { skip_on_cran() resultado <- cie_search("diabetes", threshold = 0.70) columnas_esperadas <- c("codigo", "descripcion", "score") expect_true(all(columnas_esperadas %in% names(resultado))) }) test_that("cie_map_comorbid retorna columnas esperadas", { resultado <- cie_map_comorbid(c("E11.0")) columnas_esperadas <- c("codigo", "categoria") expect_equal(names(resultado), columnas_esperadas) }) # ============================================================================== # PRUEBAS DE LIMITES Y CASOS EXTREMOS # ============================================================================== test_that("cie_search con threshold 0 no crashea", { skip_on_cran() resultado <- cie_search("diabetes", threshold = 0, max_results = 10) expect_s3_class(resultado, "tbl_df") }) test_that("cie_search con threshold 1 retorna solo exactos", { skip_on_cran() # Threshold = 1 significa match exacto suppressMessages({ resultado <- cie_search("diabetes mellitus", threshold = 1.0) }) expect_s3_class(resultado, "tbl_df") # Si hay resultados, todos deben tener score = 1 if (nrow(resultado) > 0) { expect_true(all(resultado$score == 1)) } }) test_that("cie_lookup con expansion de categoria completa", { skip_on_cran() # Expandir categoria E (muy grande) hijos <- cie_lookup("E", expandir = TRUE) expect_s3_class(hijos, "tbl_df") expect_gt(nrow(hijos), 100) }) test_that("cie_validate_vector con vector muy grande", { # Vector grande de codigos mixtos codigos <- c( rep("E11.0", 100), rep("INVALIDO", 100), rep("Z00", 100) ) resultado <- cie_validate_vector(codigos) expect_length(resultado, 300) expect_equal(sum(resultado), 200) # 100 E11.0 + 100 Z00 }) # ============================================================================== # PRUEBAS ADICIONALES get_cie10_db() # ============================================================================== test_that("get_cie10_db retorna conexion DBI valida", { skip_on_cran() get_cie10_db <- ciecl:::get_cie10_db con <- get_cie10_db() on.exit(DBI::dbDisconnect(con)) expect_true(DBI::dbIsValid(con)) expect_s4_class(con, "SQLiteConnection") }) test_that("get_cie10_db crea tabla cie10 si no existe", { skip_on_cran() get_cie10_db <- ciecl:::get_cie10_db con <- get_cie10_db() on.exit(DBI::dbDisconnect(con)) # Tabla debe existir expect_true(DBI::dbExistsTable(con, "cie10")) }) test_that("get_cie10_db tabla tiene indices", { skip_on_cran() get_cie10_db <- ciecl:::get_cie10_db con <- get_cie10_db() on.exit(DBI::dbDisconnect(con)) # Verificar que existen indices (SQLite) indices <- DBI::dbGetQuery(con, "SELECT name FROM sqlite_master WHERE type='index'") expect_gt(nrow(indices), 0) }) test_that("get_cie10_db usa directorio cache correcto", { skip_on_cran() cache_dir <- tools::R_user_dir("ciecl", "data") db_path <- file.path(cache_dir, "cie10.db") # Despues de cualquier uso del paquete, debe existir get_cie10_db <- ciecl:::get_cie10_db con <- get_cie10_db() DBI::dbDisconnect(con) expect_true(file.exists(db_path)) }) # ============================================================================== # PRUEBAS ADICIONALES cie10_clear_cache() # ============================================================================== test_that("cie10_clear_cache elimina archivo db", { skip_on_cran() cache_dir <- tools::R_user_dir("ciecl", "data") db_path <- file.path(cache_dir, "cie10.db") # Asegurar que existe get_cie10_db <- ciecl:::get_cie10_db con <- get_cie10_db() DBI::dbDisconnect(con) expect_true(file.exists(db_path)) # Limpiar cache suppressMessages(cie10_clear_cache()) expect_false(file.exists(db_path)) }) test_that("cie10_clear_cache es idempotente", { skip_on_cran() # Llamar dos veces no debe dar error expect_no_error({ suppressMessages(cie10_clear_cache()) suppressMessages(cie10_clear_cache()) }) }) test_that("cie10_clear_cache emite mensaje apropiado", { skip_on_cran() # Asegurar que existe cache get_cie10_db <- ciecl:::get_cie10_db con <- get_cie10_db() DBI::dbDisconnect(con) # Debe emitir mensaje de eliminacion expect_message(cie10_clear_cache(), "eliminado") # Segunda vez mensaje diferente expect_message(cie10_clear_cache(), "no existe") }) test_that("cie10_clear_cache retorna invisible NULL", { skip_on_cran() resultado <- suppressMessages(cie10_clear_cache()) expect_null(resultado) }) # ============================================================================== # PRUEBAS cie10_sql() ADICIONALES # ============================================================================== test_that("cie10_sql bloquea DROP TABLE", { skip_on_cran() expect_error( cie10_sql("DROP TABLE cie10"), "Solo queries SELECT" ) }) test_that("cie10_sql bloquea DELETE", { skip_on_cran() expect_error( cie10_sql("DELETE FROM cie10 WHERE codigo = 'E11.0'"), "Solo queries SELECT" ) }) test_that("cie10_sql bloquea UPDATE", { skip_on_cran() expect_error( cie10_sql("UPDATE cie10 SET descripcion = 'test' WHERE codigo = 'E11.0'"), "Solo queries SELECT" ) }) test_that("cie10_sql bloquea INSERT", { skip_on_cran() expect_error( cie10_sql("INSERT INTO cie10 VALUES ('X99', 'test', NULL, NULL, NULL, NULL, NULL, NULL, 0, 0)"), "Solo queries SELECT" ) }) test_that("cie10_sql bloquea multiples statements", { skip_on_cran() # El error puede ser por keyword no permitido (DROP) o por multiples statements expect_error( cie10_sql("SELECT * FROM cie10; DROP TABLE cie10") ) }) test_that("cie10_sql permite SELECT con subconsulta", { skip_on_cran() # Subconsulta valida resultado <- cie10_sql("SELECT * FROM cie10 WHERE codigo IN (SELECT codigo FROM cie10 LIMIT 5)") expect_s3_class(resultado, "tbl_df") }) test_that("cie10_sql permite punto y coma dentro de strings", { skip_on_cran() # Punto y coma dentro de string no debe ser bloqueado resultado <- cie10_sql("SELECT * FROM cie10 WHERE descripcion LIKE '%test; test%' LIMIT 1") expect_s3_class(resultado, "tbl_df") }) test_that("cie10_sql con close=FALSE mantiene conexion", { skip_on_cran() # Este test verifica comportamiento interno # La conexion no debe cerrarse si close=FALSE resultado <- cie10_sql("SELECT COUNT(*) as n FROM cie10", close = TRUE) expect_gt(resultado$n, 0) }) test_that("cie10_sql normaliza espacios al inicio", { skip_on_cran() # Query con espacios y saltos de linea al inicio resultado <- cie10_sql(" SELECT COUNT(*) as n FROM cie10 ") expect_s3_class(resultado, "tbl_df") expect_gt(resultado$n, 0) })