54 · Erros e famílias
15 exercícios: .
python3 exercicios/run_all.py 54Os exercícios#
| # | Título | Enunciado |
|---|---|---|
| 328 | capturar por FAMILIA, e nao por nome | sao 208 codigos de erro. Comparar nomes exatos obrigaria |
| 329 | o que se lê de um erro capturado | um erro nao e so uma mensagem. Ele carrega o tipo, a |
| 330 | o erro que o programa inventa | nao ha hierarquia de excecao do usuario, e o que |
| 331 | a familia das colecoes | indice, chave e fatia falham de jeitos diferentes, e |
| 332 | converter o que veio de fora | todo dado que entra num programa e texto — do formulario, |
| 333 | fechar o que foi aberto | 'defer' roda na saida da ACAO, onde quer que esteja |
| 334 | insistir, e saber quando parar | uma falha de rede e diferente de uma falha de logica. A |
| 335 | a falha como VALOR | um erro interrompe; um resultado nao. Quando a falha e |
| 336 | o valor que nao serve para a operacao | a familia dos tipos. O que importa aqui e que nenhuma |
| 337 | o sistema de arquivos falha de seis jeitos | nao existe, existe e nao e o que se espera, sem |
| 338 | o erro que aparece na FRONTEIRA | 'requires', 'promises' e 'invariant' movem a falha para |
| 339 | o erro que acontece em OUTRA thread | 'parallel' espera todas as tarefas e levanta na linha do |
| 340 | o que 'monitor' NAO pega | 'halt', 'skip' e 'yield' atravessam um 'monitor'. Eles |
| 341 | afirmar que algo FALHA | um teste que so confere o caminho feliz nao prova nada |
| 342 | as dezoito familias, e como escolher | fechar o modulo com o mapa. O codigo de um erro diz a |
328 · capturar por FAMILIA, e nao por nome#
Enunciado. sao 208 codigos de erro. Comparar nomes exatos obrigaria
// a listar cada um; a heranca deixa 'handle RuntimeError' pegar os
// vinte da familia, e quem precisa distinguir ainda nomeia o
// especifico.
adopt Arcane.IO as IO
action classe_de(acao):
monitor:
acao()
handle Error as e:
yield e.type
yield "nenhum"
out "== 1. cada erro tem a sua classe =="
assert classe_de(lambda => 1 / 0) is "DivisionByZeroError" // df: permitir division-by-zero
assert classe_de(lambda => [1, 2][9]) is "IndexError" // df: permitir indice-fora-do-alcance
assert classe_de(lambda => {"a": 1} ["b"]) is "KeyError" // df: permitir chave-ausente
assert classe_de(lambda => int("nao e numero")) is "ConversionError"
assert classe_de(lambda => IO.read("/nao/existe/mesmo")) is "FileNotFoundError"
out ""
out "== 2. a familia pega todos os filhos =="
action pela_familia(acao, _rotulo):
monitor:
acao()
handle RuntimeError:
yield "runtime"
handle Error:
yield "outro"
yield "nenhum"
assert pela_familia(lambda => 1 / 0, "divisao") is "runtime" // df: permitir division-by-zero
out ""
out "== 3. a ORDEM dos handle importa =="
// O primeiro que casa vence. Com 'Error' no topo, nada abaixo dele
// seria alcancado.
qual := ""
monitor:
_x := 1 / 0 // df: permitir division-by-zero
handle DivisionByZeroError:
qual := "especifico"
handle RuntimeError:
qual := "familia"
assert qual is "especifico"
invertido := ""
monitor:
_x := 1 / 0 // df: permitir division-by-zero
handle RuntimeError:
invertido := "familia"
handle DivisionByZeroError:
invertido := "especifico"
assert invertido is "familia"
out " do especifico ao geral, sempre"
out ""
out "== 4. 'trigger' levanta TriggerError, e nao RuntimeError =="
// E a armadilha mais cara: 'handle RuntimeError' nao pega um
// 'trigger'.
nao_pegou := yes
monitor:
monitor:
trigger "minha falha"
handle RuntimeError:
nao_pegou := no
handle TriggerError as e:
assert e.message is "minha falha"
assert nao_pegou
// Para pegar qualquer coisa: 'handle Error'.
pegou := no
monitor:
trigger "outra"
handle Error:
pegou := yes
assert pegou
out ""
out "== 5. e 'handle' sem tipo pega tudo =="
sem_tipo := no
monitor:
trigger "qualquer"
handle:
sem_tipo := yes
assert sem_tipo
out "exercicio 328 ok"São mais de duzentos códigos. Comparar nomes exatos obrigaria a listar cada um; a herança deixa handle RuntimeError pegar os vinte da família, e quem precisa distinguir ainda nomeia o específico.
A ORDEM dos `handle` importa#
O primeiro que casa vence. Com Error no topo, nada abaixo dele seria alcançado — do específico ao geral, sempre.
`trigger` levanta `TriggerError`, e não `RuntimeError`#
É a armadilha mais cara do capítulo: handle RuntimeError não pega um trigger, e o sintoma é "o tratamento não funcionou" sem nenhuma pista.
E `handle` sem tipo pega tudo#
Inclusive o que você não queria. Ele serve para o topo do programa, e não para o meio.
329 · o que se lê de um erro capturado#
Enunciado. um erro nao e so uma mensagem. Ele carrega o tipo, a
// linha, a nota, a dica e a doc — e e isso que faz a diferenca entre
// um log que ajuda e um que so diz que algo deu errado.
adopt Arcane.IO as IO
out "== 1. os campos =="
monitor:
_x := 1 / 0 // df: permitir division-by-zero
handle Error as e:
assert e.type is "DivisionByZeroError"
assert "zero" in lower(e.message)
assert e.line bigger 0
out $" {e.type} na linha {e.line}: {e.message}"
out ""
out "== 2. a nota e a dica, quando existem =="
monitor:
IO.read("/nao/existe/mesmo/nem/um/pouco")
handle FileNotFoundError as e:
assert len(e.message) bigger 0
out $" {e.message}"
out ""
out "== 3. relancar preservando o original =="
action camada_de_baixo():
trigger "o banco nao respondeu"
action camada_de_cima():
monitor:
camada_de_baixo()
handle Error as e:
// Embrulhar e a norma; sem a causa, a mensagem de fora diz o
// QUE falhou e perde o PORQUE.
trigger $"nao consegui salvar o pedido: {e.message}"
monitor:
camada_de_cima()
handle Error as e:
assert "salvar o pedido" in e.message
assert "banco nao respondeu" in e.message
out $" {e.message}"
out ""
out "== 4. 'ensure' roda sempre =="
passos := []
action com_limpeza(vai_falhar):
monitor:
passos.append("comecou")
given vai_falhar:
trigger "falhou"
passos.append("terminou")
handle Error:
passos.append("tratou")
ensure:
passos.append("limpou")
com_limpeza(no)
assert passos is ["comecou", "terminou", "limpou"]
passos := []
com_limpeza(yes)
assert passos is ["comecou", "tratou", "limpou"]
out ""
out "== 5. e 'monitor' SEM 'handle' nao engole =="
escapou := no
monitor:
monitor:
trigger "passa direto"
ensure:
passos.append("mas o ensure rodou")
handle Error as e:
escapou := yes
assert e.message is "passa direto"
assert escapou
assert passos[-1] is "mas o ensure rodou"
out "exercicio 329 ok"Um erro não é só uma mensagem: ele carrega o tipo, a linha, a nota, a dica e o código. É isso que faz a diferença entre um log que ajuda e um que só diz que algo deu errado.
Relançar preservando o original#
Embrulhar é a norma; sem a causa, a mensagem de fora diz o quê falhou e perde o porquê.
`ensure` roda sempre#
Nos dois caminhos, e depois do handle.
E `monitor` SEM `handle` não engole#
Ele só garante o ensure. O erro continua viajando.
330 · o erro que o programa inventa#
Enunciado. nao ha hierarquia de excecao do usuario, e o que
// substitui e melhor para quem trata — um RECORD com os campos do
// problema. Uma classe vazia so carrega o nome; o record leva o saldo,
// o valor pedido e a diferenca. E 'handle <Nome>' o captura pelo nome.
record SaldoInsuficiente:
saldo: Float
pedido: Float
action falta() -> Float:
yield self.pedido - self.saldo
record ContaBloqueada:
conta: String
motivo: String
record ValorInvalido:
valor: Float
out "== 1. levantar um record =="
action sacar(saldo, quanto):
given quanto bigger saldo:
trigger SaldoInsuficiente(saldo, quanto)
yield saldo - quanto
assert sacar(100.0, 30.0) is 70.0
out ""
out "== 2. e captura-lo PELO NOME =="
pegou := no
monitor:
sacar(100.0, 250.0)
handle SaldoInsuficiente as e:
pegou := yes
// O valor original viaja em 'e.value': os campos, e nao o texto.
assert e.value.falta() is 150.0
out $" faltam {e.value.falta()}"
assert pegou
out ""
out "== 3. cada nome captura so o seu =="
action transferir(de, quanto, bloqueadas):
given de in bloqueadas:
trigger ContaBloqueada(de, "em analise")
given quanto smaller_eq 0.0:
trigger ValorInvalido(quanto)
given quanto bigger 1000.0:
trigger SaldoInsuficiente(1000.0, quanto)
yield quanto
action tentar(de, quanto):
monitor:
transferir(de, quanto, ["X"])
yield "ok"
handle ContaBloqueada as e:
yield $"bloqueio: {e.value.conta} ({e.value.motivo})"
handle ValorInvalido as e:
yield $"valor invalido: {e.value.valor}"
handle SaldoInsuficiente as e:
yield $"faltam {e.value.falta()}"
assert tentar("A", 100.0) is "ok"
assert startswith(tentar("X", 100.0), "bloqueio:")
assert startswith(tentar("A", -5.0), "valor invalido:")
assert startswith(tentar("A", 5000.0), "faltam")
cycle caso in [["A", 100.0], ["X", 100.0], ["A", -5.0], ["A", 5000.0]]:
out $" {caso[0]} {caso[1]} -> {tentar(caso[0], caso[1])}"
out ""
out "== 4. e 'handle Error' continua pegando todos =="
qualquer := ""
monitor:
sacar(10.0, 20.0)
handle Error as e:
// O tipo do ERRO continua sendo TriggerError; o nome do record e
// o que 'handle' casa.
qualquer := e.type
assert qualquer is "TriggerError"
out ""
out "== 5. 'match' sobre o valor, quando se quer decidir por forma =="
action classificar(falha):
match falha:
point SaldoInsuficiente(saldo, pedido):
yield $"faltam {pedido - saldo}"
point ContaBloqueada(conta, motivo):
yield $"{conta}: {motivo}"
point String as texto:
yield $"texto: {texto}"
default:
yield "desconhecido"
assert classificar(SaldoInsuficiente(10.0, 30.0)) is "faltam 20.0"
assert classificar(ContaBloqueada("X", "em analise")) is "X: em analise"
assert classificar("algo") is "texto: algo"
out ""
out "== 6. um texto levantado NAO ganha nome =="
// Senao 'handle String' capturaria todo 'trigger "..."' do programa.
so_texto := no
monitor:
monitor:
trigger "so uma mensagem"
handle String:
assert no
handle Error as e:
so_texto := e.type is "TriggerError"
assert so_texto
out "exercicio 330 ok"Não há hierarquia de exceção do usuário, e o que substitui é melhor para quem trata: um record com os campos do problema. Uma classe vazia só carrega o nome; o record leva o saldo, o valor pedido e a diferença.
E `handle <Nome>` o captura pelo nome#
Isso não funcionava: _error_matches consultava tipo_usuario e nada o escrevia. O ramo existia, tinha docstring, e era inalcançável — quem levantava um record só podia capturá-lo com handle Error e um match.
O valor original viaja em `e.value`#
Os campos, e não o texto. Extrair por regex o que o programa já tinha como dado é o que isso evita.
E um texto levantado NÃO ganha nome#
Senão handle String capturaria todo trigger "…" do programa.
331 · a familia das colecoes#
Enunciado. indice, chave e fatia falham de jeitos diferentes, e
// cada um tem uma saida diferente. Tratar os tres com o mesmo
// 'handle Error' e o que faz um bug de chave virar "algo deu errado".
out "== 1. indice fora do alcance =="
xs := [10, 20, 30]
monitor:
_x := xs[9]
handle IndexError as e:
assert "9" in e.message or "range" in lower(e.message)
out $" {e.message}"
// A saida: conferir o tamanho, ou usar o indice negativo.
assert xs[-1] is 30
assert(xs[9] given len(xs) bigger 9 otherwise void) is void
out ""
out "== 2. chave ausente =="
v := {"nome": "Ana", "cidade": "Florianopolis"}
monitor:
_x := v["idade"] // df: permitir chave-ausente
handle KeyError as e:
out $" {e.message}"
// A saida: '??' — e e ela que o proprio erro sugere.
assert(v["idade"] ?? 0) is 0
assert v["nome"] ?? "sem nome" is "Ana"
out ""
out "== 3. e a diferenca entre 'pop' e 'remove' =="
// 'remove' apaga no lugar e e silencioso quando nao acha.
copia := {"a": 1, "b": 2}
remove(copia, "z")
assert keys(copia) is ["a", "b"]
// 'pop' devolve o valor e por isso LEVANTA: devolver void calado
// esconderia a diferenca entre "a chave valia void" e "a chave nao
// estava la".
levantou := no
monitor:
pop(copia, "z")
handle Error:
levantou := yes
assert levantou
assert pop(copia, "a") is 1
out ""
out "== 4. num cluster, o segundo argumento e o VALOR =="
lista := [1, 2, 3]
remove(lista, 2)
assert lista is [1, 3]
vault := {"x": 1, "y": 2}
remove(vault, "x")
assert keys(vault) is ["y"]
out ""
out "== 5. 'omit' devolve copia, e nao mexe no original =="
steady ORIGINAL := {"a": 1, "b": 2, "c": 3}
sem_b := omit(ORIGINAL, "b")
assert keys(sem_b) is ["a", "c"]
assert keys(ORIGINAL) is ["a", "b", "c"]
out ""
out "== 6. e uma colecao congelada recusa a escrita =="
congelada := freeze([1, 2, 3])
recusou := no
monitor:
congelada.append(4)
handle Error as e:
recusou := yes
assert recusou
assert len(congelada) is 3
out "exercicio 331 ok"Índice, chave e fatia falham de jeitos diferentes, e cada um tem uma saída diferente. Tratar os três com o mesmo handle Error é o que faz um bug de chave virar "algo deu errado".
`remove` e `pop` mudam de sentido#
Num cluster o segundo argumento é o valor; num vault, a chave. remove é silencioso; pop levanta — devolver void calado esconderia a diferença entre "a chave valia void" e "a chave não estava lá".
`omit` devolve cópia#
E não mexe no original.
E o `??` é a saída que o próprio erro sugere#
Um analisador que acusasse o conserto que ele mesmo recomenda é um analisador que se desliga.
332 · converter o que veio de fora#
Enunciado. todo dado que entra num programa e texto — do formulario,
// do CSV, da variavel de ambiente. Converter e onde ele falha, e
// converter em silencio e onde ele mente.
adopt Arcane.Serialization as Serde
out "== 1. a conversao que falha =="
monitor:
int("quarenta e dois")
handle ConversionError as e:
out $" {e.message}"
assert int("42") is 42
assert float("3.5") is 3.5
out ""
out "== 2. converter com um padrao, sem esconder o erro =="
action para_inteiro(texto, padrao):
monitor:
yield int(texto)
handle ConversionError:
yield padrao
assert para_inteiro("42", 0) is 42
assert para_inteiro("x", 0) is 0
out ""
out "== 3. a diferenca entre nao informado e invalido =="
// Um padrao silencioso junta os dois casos, e o relatorio sai com
// zeros que ninguem sabe de onde vieram.
action ler_idade(texto):
given texto is void or trim(texto) is "":
yield {"ok": yes, "valor": void, "motivo": "nao informado"}
monitor:
yield {"ok": yes, "valor": int(texto), "motivo": ""}
handle ConversionError:
yield {"ok": no, "valor": void, "motivo": $"'{texto}' nao e um numero"}
assert ler_idade("30")["valor"] is 30
assert ler_idade("")["ok"] and ler_idade("")["valor"] is void
assert not ler_idade("trinta")["ok"]
out $" {ler_idade('trinta')['motivo']}"
out ""
out "== 4. um formulario inteiro, com os erros juntos =="
steady CAMPOS := [["idade", "30"], ["altura", "1,75"], ["peso", "70.5"]]
erros := []
valores := {}
cycle campo in CAMPOS:
monitor:
valores[campo[0]] := float(campo[1])
handle ConversionError:
erros.append($"{campo[0]}: '{campo[1]}' nao e um numero")
assert len(erros) is 1
assert "altura" in erros[0]
assert valores["peso"] is 70.5
out $" {erros[0]}"
// Juntar os erros e devolver todos de uma vez e melhor que parar no
// primeiro: quem preenche corrige tudo numa passada.
out ""
out "== 5. JSON invalido =="
assert Serde.from_json('{"a": 1}')["a"] is 1
quebrou := no
monitor:
Serde.from_json("{isto nao e json}")
handle Error as e:
quebrou := yes
out $" {e.type}"
assert quebrou
out ""
out "== 6. e a conversao que NAO e recusada e a mais perigosa =="
// Float aceita notacao cientifica e infinito.
assert float("1e400") bigger 1000000.0
assert int("0") is 0
out " 'int' e 'float' aceitam mais do que um formulario deveria"
out "exercicio 332 ok"Todo dado que entra num programa é texto — do formulário, do CSV, da variável de ambiente. Converter é onde ele falha, e converter em silêncio é onde ele mente.
A diferença entre não informado e inválido#
Um padrão silencioso junta os dois casos, e o relatório sai com zeros que ninguém sabe de onde vieram.
Juntar os erros e devolver todos#
Melhor que parar no primeiro: quem preenche corrige tudo numa passada.
E a conversão que NÃO é recusada é a mais perigosa#
float aceita notação científica e infinito — mais do que um formulário deveria.
333 · fechar o que foi aberto#
Enunciado. 'defer' roda na saida da ACAO, onde quer que esteja
// escrito — dentro de um cycle, de um persist, ou no topo. E ele NAO
// engole: um erro dentro dele viaja, e se a acao ja estava falhando,
// viaja o ORIGINAL com o do defer em 'e.outros'.
adopt Arcane.IO as IO
adopt Arcane.OS as OS
steady pasta := $"{OS.temp_dir()}/df-333-{randint(100000, 999999)}"
IO.mkdir(pasta)
ordem := []
out "== 1. ele roda na saida da acao =="
action com_defer():
defer:
ordem.append("fechou")
ordem.append("trabalhou")
yield "pronto"
assert com_defer() is "pronto"
assert ordem is ["trabalhou", "fechou"]
out ""
out "== 2. e roda tambem quando a acao FALHA =="
ordem := []
action falha_com_defer():
defer:
ordem.append("fechou mesmo assim")
trigger "quebrou no meio"
monitor:
falha_com_defer()
handle Error:
ordem.append("tratado")
assert ordem is ["fechou mesmo assim", "tratado"]
out ""
out "== 3. num laco, ele acumula e roda TUDO no fim =="
// Ele se registrava no escopo em que aparece, e so o da acao era
// consultado: um 'defer' num laco NUNCA rodava, calado — e fechar
// arquivo por volta e o uso mais obvio que existe. Hoje ele roda, e
// roda na SAIDA DA ACAO, na ordem inversa do registro.
ordem := []
action num_laco():
cycle i from 1 to 3:
defer:
ordem.append($"fecha {i}")
ordem.append($"abre {i}")
num_laco()
assert ordem is ["abre 1", "abre 2", "abre 3", "fecha 3", "fecha 2", "fecha 1"]
out $" {ordem}"
// A consequencia pratica: num laco de dez mil voltas, dez mil
// arquivos ficam abertos ate a acao terminar. Quem precisa fechar por
// VOLTA extrai o corpo para uma acao propria — e ai a saida dela e o
// fim do 'defer'.
por_volta := []
action uma_volta(i):
defer:
por_volta.append($"fecha {i}")
por_volta.append($"abre {i}")
cycle i from 1 to 3:
uma_volta(i)
assert por_volta is ["abre 1", "fecha 1", "abre 2", "fecha 2",
"abre 3", "fecha 3"]
out $" por acao: {por_volta}"
out ""
out "== 4. um arquivo por volta, fechado por volta =="
action gravar_tres():
cycle i from 1 to 3:
caminho := $"{pasta}/parte-{i}.txt"
defer:
ordem.append($"salvou {i}")
IO.write(caminho, $"conteudo {i}")
ordem := []
gravar_tres()
assert len(ordem) is 3
cycle i from 1 to 3:
assert IO.exists($"{pasta}/parte-{i}.txt")
out ""
out "== 5. o defer NAO engole o erro dele =="
action defer_que_quebra():
defer:
quebrar()
yield "voltei"
action quebrar():
trigger "o fechamento falhou"
viajou := no
monitor:
defer_que_quebra()
handle Error as e:
viajou := yes
assert "fechamento falhou" in e.message
assert viajou
out " um arquivo nao fechado nao termina o programa com codigo 0"
out ""
out "== 6. e quando os DOIS falham, viaja o original =="
action os_dois():
defer:
quebrar()
trigger "o erro de verdade"
monitor:
os_dois()
handle Error as e:
// E o modelo do try-with-resources: o original manda, e o do
// fechamento vai junto.
assert "o erro de verdade" in e.message
assert len(e.outros) bigger_eq 1
out $" original: {e.message} · junto: {len(e.outros)}"
IO.remove_tree(pasta)
out "exercicio 333 ok"defer roda na saída da ação, onde quer que esteja escrito.
Num laço, ele acumula e roda tudo no fim#
Na ordem inversa do registro. A consequência prática: num laço de dez mil voltas, dez mil arquivos ficam abertos até a ação terminar. Quem precisa fechar por volta extrai o corpo para uma ação própria.
Ele NÃO engole o erro dele#
Um erro dentro do defer viaja. Até a correção era descartado, e um arquivo não fechado terminava o programa com código 0.
E quando os dois falham, viaja o original#
Com o do fechamento em e.outros — é o modelo do try-with-resources.
334 · insistir, e saber quando parar#
Enunciado. uma falha de rede e diferente de uma falha de logica. A
// primeira costuma passar na segunda tentativa; a segunda passa a
// vida inteira falhando. Insistir na errada esconde o bug.
adopt Arcane.Time as Tempo
out "== 1. um retry a mao =="
tentativas := {"n": 0}
action instavel(falhar_ate):
tentativas["n"] := tentativas["n"] + 1
given tentativas["n"] smaller_eq falhar_ate:
trigger $"falha transitoria {tentativas['n']}"
yield $"ok na tentativa {tentativas['n']}"
action insistir(acao, vezes):
ultimo := ""
cycle i from 1 to vezes:
monitor:
yield acao()
handle Error as e:
ultimo := e.message
trigger $"desisti depois de {vezes}: {ultimo}"
assert insistir(lambda => instavel(2), 5) is "ok na tentativa 3"
assert tentativas["n"] is 3
out ""
out "== 2. e ele desiste, dizendo o ultimo motivo =="
tentativas["n"] := 0
desistiu := no
monitor:
insistir(lambda => instavel(99), 3)
handle Error as e:
desistiu := yes
assert "desisti depois de 3" in e.message
assert "falha transitoria 3" in e.message
out $" {e.message}"
assert desistiu
out ""
out "== 3. o recuo com TREMOR =="
// Sem jitter, todos os clientes que falharam juntos voltam juntos — e
// o servico cai de novo no mesmo instante.
action recuo(tentativa, base):
espera := base * (2 ** (tentativa - 1))
yield espera + random() * base
esperas := [recuo(i, 0.01) cycle i in range(1, 5)]
assert esperas[0] smaller esperas[3]
assert len(unique([round(recuo(1, 0.01), 6) cycle i in range(5)])) bigger 1
out " quatro tentativas, esperas crescentes e diferentes entre si"
out ""
out "== 4. o que NAO se repete =="
// Um erro de logica repete identico. Repetir aqui e gastar tempo e
// esconder o bug.
action nao_vale_repetir():
x := [1, 2]
yield x[9] // df: permitir indice-fora-do-alcance
sempre := 0
cycle i from 1 to 3:
monitor:
nao_vale_repetir()
handle IndexError:
sempre += 1
assert sempre is 3
out " tres tentativas, tres vezes o MESMO erro"
out ""
out "== 5. distinguir as duas familias =="
steady REPETIVEIS := ["ConnectionError", "TimeoutError", "NetworkError"]
action vale_repetir(tipo):
yield tipo in REPETIVEIS
assert vale_repetir("TimeoutError")
assert not vale_repetir("IndexError")
assert not vale_repetir("ConversionError")
out ""
out "== 6. e um prazo total, alem do numero de tentativas =="
action insistir_ate(acao, prazo):
limite := Tempo.timestamp() + prazo
ultimo := ""
persist Tempo.timestamp() smaller limite:
monitor:
yield acao()
handle Error as e:
ultimo := e.message
trigger $"prazo esgotado: {ultimo}"
tentativas["n"] := 0
assert insistir_ate(lambda => instavel(1), 2.0) is "ok na tentativa 2"
out "exercicio 334 ok"Uma falha de rede é diferente de uma falha de lógica. A primeira costuma passar na segunda tentativa; a segunda passa a vida inteira falhando — e insistir nela esconde o bug.
O recuo com TREMOR#
Sem jitter, todos os clientes que falharam juntos voltam juntos — e o serviço cai de novo no mesmo instante.
O que NÃO se repete#
Um erro de lógica repete idêntico. Três tentativas, três vezes o mesmo erro.
E um prazo total, além do número de tentativas#
Cinco tentativas de dez segundos são cinquenta segundos de espera para quem está do outro lado.
335 · a falha como VALOR#
Enunciado. um erro interrompe; um resultado nao. Quando a falha e
// ESPERADA — um CEP que nao existe, um arquivo que pode faltar —
// levantar obriga quem chama a montar um 'monitor' em volta de cada
// chamada, e o codigo some dentro do tratamento.
adopt Arcane.Resultado as Res
out "== 1. ok e falha =="
action dividir(a, b):
given b is 0:
yield Res.falha("divisao por zero")
yield Res.ok(a / b)
bom := dividir(10, 2)
assert bom.deu_certo()
assert bom.valor() is 5.0
ruim := dividir(10, 0)
assert ruim.falhou()
assert ruim.erro() is "divisao por zero"
out $" {ruim.erro()}"
out ""
out "== 2. encadear sem 'monitor' em cada passo =="
action metade(x):
yield dividir(x, 2)
action mais_um(x):
yield Res.ok(x + 1)
passo := dividir(20, 2).entao(metade).entao(mais_um)
assert passo.valor() is 6.0
// E a falha ATRAVESSA sem que ninguem confira no meio.
quebrado := dividir(20, 0).entao(metade).entao(mais_um)
assert quebrado.falhou()
assert quebrado.erro() is "divisao por zero"
out " a falha atravessou tres passos sem um 'given' sequer"
out ""
out "== 3. mapear muda o VALOR, e nao o desfecho =="
assert dividir(10, 2).mapear(lambda v => v * 10).valor() is 50.0
assert dividir(10, 0).mapear(lambda v => v * 10).falhou()
out ""
out "== 4. o padrao de saida =="
assert dividir(10, 0).ou(-1.0) is -1.0
assert dividir(10, 2).ou(-1.0) is 5.0
// E a recuperacao, que transforma a falha em outro resultado.
recuperado := dividir(10, 0).recuperar(lambda _e => Res.ok(0.0))
assert recuperado.valor() is 0.0
out ""
out "== 5. a hora de voltar para o mundo dos erros =="
// Na fronteira do programa, um resultado vira erro — porque ai nao ha
// mais quem trate.
levantou := no
monitor:
dividir(1, 0).exigir()
handle Error as e:
levantou := yes
assert "divisao por zero" in e.message
assert levantou
out ""
out "== 6. 'tentar' embrulha uma acao que LEVANTA =="
// E a ponte no outro sentido: transformar um erro em resultado.
assert Res.tentar(lambda => 1 / 0).falhou() // df: permitir division-by-zero
assert Res.tentar(lambda => 10 / 2).valor() is 5.0
out ""
out "== 7. 'Talvez', para onde 'void' e ambiguo =="
// 'void' nao distingue "nao achei" de "achei, e o valor e void".
steady CACHE := {"a": 1, "b": void}
action buscar(chave):
given chave not in CACHE:
yield Res.nada()
yield Res.algo(CACHE[chave])
assert buscar("a").tem()
assert buscar("b").tem() // achei, e o valor e void
assert buscar("z").vazio() // nao achei
out " 'b' existe e vale void; 'z' nao existe"
out ""
out "== 8. e todos/primeiro, para uma lista de resultados =="
steady VARIOS := [dividir(10, 2), dividir(10, 0), dividir(20, 4)]
assert len(Res.erros(VARIOS)) is 1
assert Res.todos(VARIOS).falhou()
steady SO_BONS := [dividir(10, 2), dividir(20, 4)]
assert Res.todos(SO_BONS).valor() is [5.0, 5.0]
out ""
out "== 9. quando usar cada um =="
out " falha ESPERADA (CEP invalido, arquivo opcional): Resultado"
out " falha EXCEPCIONAL (disco cheio, bug): trigger"
out "exercicio 335 ok"Um erro interrompe; um resultado não. Quando a falha é esperada — um CEP que não existe, um arquivo que pode faltar — levantar obriga quem chama a montar um monitor em volta de cada chamada, e o código some dentro do tratamento.
A falha atravessa a cadeia sem um `given` sequer#
entao só chama o próximo quando o anterior deu certo.
E a hora de voltar para o mundo dos erros#
Na fronteira do programa, exigir() transforma o resultado em erro — porque ali não há mais quem trate. E tentar é a ponte no outro sentido.
`Talvez`, para onde `void` é ambíguo#
void não distingue "não achei" de "achei, e o valor é void".
Quando usar cada um#
Falha esperada: Resultado. Falha excepcional (disco cheio, bug): trigger.
336 · o valor que nao serve para a operacao#
Enunciado. a familia dos tipos. O que importa aqui e que nenhuma
// mensagem cita tipo do Python: 'int', 'str', 'list' e 'dict' nao
// existem nesta linguagem, e uma mensagem nesses termos manda a pessoa
// procurar na documentacao errada.
out "== 1. somar o que nao soma =="
monitor:
x := 1 + "dois"
handle TypeError as e:
assert "int" not in e.message
assert "str" not in e.message
out $" {e.message}"
out ""
out "== 2. os nomes sao os DESTA linguagem =="
assert typeof(1) is "Integer"
assert typeof(1.5) is "Float"
assert typeof("x") is "String"
assert typeof([1]) is "Cluster"
assert typeof({"a": 1}) is "Vault"
assert typeof(yes) is "Boolean"
assert typeof(void) is "Void"
assert typeof((1, "a")) is "Tuple"
out ""
out "== 3. chamar o que nao e acao =="
monitor:
x := 42
x()
handle Error as e:
out $" {e.type}"
out ""
out "== 4. indexar o que nao e indexavel =="
monitor:
y := 42
_z := y[0] // df: permitir index-type
handle Error as e:
assert "Integer" in e.message or "index" in lower(e.message)
out $" {e.message}"
out ""
out "== 5. o void que atravessa =="
// 'void' nao levanta na hora: ele viaja, e a queixa sai longe da
// causa. E por isso que '?.' e '??' existem.
action pode_nao_achar(chave):
given chave is "a":
yield {"nome": "Ana"}
yield void
achado := pode_nao_achar("z")
assert achado is void
monitor:
_nome := achado["nome"]
handle Error as e:
out $" {e.message}"
// As duas saidas:
assert(achado?.nome) is void
assert(pode_nao_achar("z") ?? {"nome": "desconhecido"})["nome"] is "desconhecido"
out ""
out "== 6. e a aridade errada e recusada na chamada =="
action somar(a, b):
yield a + b
// 'somar(1)' e acusado pelo 'check' ANTES de rodar — e por isso nao
// da para escreve-lo aqui sem o arquivo inteiro ser recusado. A forma
// que so falha em execucao e a chamada indireta.
chamar := somar
monitor:
chamar(1)
handle TypeError as e:
assert "b" in e.message
out $" {e.message}"
monitor:
chamar(1, 2, 3)
handle TypeError as e:
out $" {e.message}"
out "exercicio 336 ok"O que importa aqui é que nenhuma mensagem cita tipo do Python: int, str, list e dict não existem nesta linguagem, e uma mensagem nesses termos manda a pessoa procurar na documentação errada.
Os nomes são os DESTA linguagem#
Integer, Cluster, Vault, Boolean, Void, Tuple.
O `void` que atravessa#
Ele não levanta na hora: viaja, e a queixa sai longe da causa. É por isso que ?. e ?? existem.
E a aridade errada é recusada na chamada#
E antes dela, quando o check consegue provar.
337 · o sistema de arquivos falha de seis jeitos#
Enunciado. nao existe, existe e nao e o que se espera, sem
// permissao, e uma pasta onde se queria um arquivo. Tratar os quatro
// como "erro de IO" e o que faz um log dizer "falhou" e nada mais.
adopt Arcane.IO as IO
adopt Arcane.OS as OS
steady pasta := $"{OS.temp_dir()}/df-337-{randint(100000, 999999)}"
IO.mkdir(pasta)
steady arquivo := $"{pasta}/dados.txt"
IO.write(arquivo, "conteudo")
out "== 1. nao existe =="
monitor:
IO.read($"{pasta}/nao-existe.txt")
handle FileNotFoundError as e:
out $" {e.type}"
out ""
out "== 2. e uma pasta onde se queria arquivo =="
monitor:
IO.read(pasta)
handle Error as e:
out $" {e.type}"
out ""
out "== 3. conferir ANTES e melhor do que tratar depois =="
action ler_se_der(caminho):
given not IO.exists(caminho):
yield {"ok": no, "motivo": "o arquivo nao existe"}
given caminho in [pasta]:
yield {"ok": no, "motivo": "isto e uma pasta"}
monitor:
yield {"ok": yes, "texto": IO.read(caminho)}
handle Error as e:
yield {"ok": no, "motivo": e.message}
assert ler_se_der(arquivo)["texto"] is "conteudo"
assert ler_se_der($"{pasta}/x")["motivo"] is "o arquivo nao existe"
assert ler_se_der(pasta)["motivo"] is "isto e uma pasta"
// Mas conferir e depois usar tem uma janela entre os dois: o arquivo
// pode sumir nesse intervalo. Por isso o 'monitor' continua ali.
out " conferir nao dispensa tratar: ha uma janela entre os dois"
out ""
out "== 4. escrever numa pasta que nao existe =="
monitor:
IO.write($"{pasta}/sem/pasta/x.txt", "x")
handle Error as e:
out $" {e.type}"
// A saida: criar a arvore antes.
IO.mkdir($"{pasta}/sub")
IO.write($"{pasta}/sub/x.txt", "x")
assert IO.exists($"{pasta}/sub/x.txt")
out ""
out "== 5. o defer garante a limpeza =="
action trabalhar_com_temporario():
temporario := $"{pasta}/temp-{randint(1000, 9999)}.txt"
defer:
given IO.exists(temporario):
IO.delete(temporario)
IO.write(temporario, "rascunho")
assert IO.exists(temporario)
trigger "falhou depois de criar"
monitor:
trabalhar_com_temporario()
handle Error:
out " falhou, e o temporario foi apagado"
sobraram := [f cycle f in IO.list_dir(pasta) given startswith(f, "temp-")]
assert len(sobraram) is 0
out ""
out "== 6. e o que NAO se deve fazer =="
// 'IO.remove_tree(OS.temp_dir())' destroi o temporario de TODO
// processo da maquina. Sempre uma subpasta propria.
assert startswith(pasta, OS.temp_dir())
assert pasta isnt OS.temp_dir()
IO.remove_tree(pasta)
assert not IO.exists(pasta)
out "exercicio 337 ok"Não existe, existe e não é o que se espera, sem permissão, e uma pasta onde se queria um arquivo. Tratar todos como "erro de IO" é o que faz um log dizer "falhou" e nada mais.
Conferir antes NÃO dispensa tratar#
Há uma janela entre os dois: o arquivo pode sumir nesse intervalo.
O `defer` garante a limpeza#
Inclusive no caminho de erro — que é o caminho em que o temporário costuma ficar para trás.
E o que NÃO se deve fazer#
IO.remove_tree(OS.temp_dir()) destrói o temporário de todo processo da máquina. Sempre uma subpasta própria.
338 · o erro que aparece na FRONTEIRA#
Enunciado. 'requires', 'promises' e 'invariant' movem a falha para
// onde ela e compreensivel. Sem eles, um argumento errado vira uma
// conta errada tres chamadas adiante, e a queixa fala de outra coisa.
blueprint Conta:
saldo := 0.0
invariant self.saldo bigger_eq 0.0
action setup(inicial):
self.saldo := inicial
action sacar(quanto):
expects quanto bigger 0.0
promises outcome smaller_eq before(self.saldo)
self.saldo := self.saldo - quanto
yield self.saldo
action depositar(quanto):
expects quanto bigger 0.0
self.saldo := self.saldo + quanto
yield self.saldo
out "== 1. o caminho feliz =="
c := spawn Conta(100.0)
assert c.sacar(30.0) is 70.0
assert c.depositar(10.0) is 80.0
out ""
out "== 2. a pre-condicao acusa QUEM CHAMOU =="
// Sem ela, 'sacar(-50)' AUMENTA o saldo, e o erro aparece no extrato.
pre := no
monitor:
c.sacar(-50.0)
handle PreconditionError as e:
pre := yes
out $" {e.message}"
assert pre
assert c.saldo is 80.0
out ""
out "== 3. a invariante acusa o OBJETO =="
inv := no
monitor:
c.sacar(999.0)
handle InvariantError as e:
inv := yes
out $" {e.message}"
assert inv
out ""
out "== 4. as duas dizem coisas diferentes =="
// 'PreconditionError' = o argumento esta errado.
// 'InvariantError' = o objeto chegou a um estado que nao pode existir.
out " precondicao: o erro e de quem chamou"
out " invariante: o erro e do objeto"
out ""
out "== 5. e a pos-condicao confere o RESULTADO =="
blueprint Quebrada:
valor := 0
action dobrar(x):
promises outcome bigger x
yield x - 1 // mente sobre o que promete
q := spawn Quebrada()
pos := no
monitor:
q.dobrar(10)
handle PostconditionError as e:
pos := yes
out $" {e.message}"
assert pos
out ""
out "== 6. a familia dos contratos =="
action classe_de(acao):
monitor:
acao()
handle Error as e:
yield e.type
yield "nenhum"
assert classe_de(lambda => c.sacar(-1.0)) is "PreconditionError"
assert classe_de(lambda => q.dobrar(1)) is "PostconditionError"
// E 'handle ContractError' pega os tres.
pela_base := no
monitor:
c.sacar(-1.0)
handle ContractError:
pela_base := yes
assert pela_base
out "exercicio 338 ok"expects, promises e invariant movem a falha para onde ela é compreensível. Sem eles, um argumento errado vira uma conta errada três chamadas adiante, e a queixa fala de outra coisa.
As três dizem coisas diferentes#
PreconditionError = o argumento está errado, e a culpa é de quem chamou. PostconditionError = a ação mentiu sobre o que promete. InvariantError = o objeto chegou a um estado que não pode existir.
Sem a pré-condição, `sacar(-50)` AUMENTA o saldo#
E o erro aparece no extrato, semanas depois.
E `handle ContractError` pega as três#
Quando a distinção não importa para quem trata.
339 · o erro que acontece em OUTRA thread#
Enunciado. 'parallel' espera todas as tarefas e levanta na linha do
// bloco, com as demais falhas em '.outros' — entao um 'monitor' o
// pega. 'thread' nao espera: o erro e desenhado na hora, e o programa
// termina com codigo diferente de zero.
adopt Arcane.Concurrent as Conc
out "== 1. 'parallel' espera todas =="
resultados := []
action trabalhar(n):
resultados.append(n)
parallel:
trabalhar(1)
trabalhar(2)
trabalhar(3)
assert len(resultados) is 3
out $" tres tarefas, {len(resultados)} resultados"
out ""
out "== 2. e o erro de uma delas CHEGA ao monitor =="
// Ate a correcao, os dois faziam 'except Exception' e imprimiam uma
// linha: o programa seguia, saia com 0, e nenhum 'handle' via o erro.
action quebrar(qual):
trigger $"tarefa {qual} falhou"
pegou := no
monitor:
parallel:
trabalhar(9)
quebrar("A")
handle Error as e:
pegou := yes
assert "falhou" in e.message
out $" {e.message}"
assert pegou
out ""
out "== 3. varias falhas: a primeira manda, as outras vao junto =="
monitor:
parallel:
quebrar("A")
quebrar("B")
quebrar("C")
handle Error as e:
out $" uma na mensagem, {len(e.outros)} em '.outros'"
assert len(e.outros) bigger_eq 1
out ""
out "== 4. a linguagem NAO sincroniza sozinha =="
// Duas threads escrevendo no mesmo nome perdem atualizacoes, em
// silencio. E o 'check' avisa sobre o padrao — mas avisar nao e
// sincronizar.
solto := {"n": 0}
action somar_sem_trava(voltas):
cycle i from 1 to voltas:
atual := solto["n"]
solto["n"] := atual + 1
steady THREADS := 4
steady VOLTAS := 3000
parallel:
somar_sem_trava(VOLTAS)
somar_sem_trava(VOLTAS)
somar_sem_trava(VOLTAS)
somar_sem_trava(VOLTAS)
esperado := THREADS * VOLTAS
out $" sem mutex: {solto['n']} de {esperado}"
assert solto["n"] smaller_eq esperado
out ""
out "== 5. com mutex, fecha =="
protegido := {"n": 0}
steady trava := Conc.mutex()
action somar_com_trava(voltas):
cycle i from 1 to voltas:
trava.acquire()
protegido["n"] := protegido["n"] + 1
trava.release()
parallel:
somar_com_trava(VOLTAS)
somar_com_trava(VOLTAS)
somar_com_trava(VOLTAS)
somar_com_trava(VOLTAS)
assert protegido["n"] is esperado
out $" com mutex: {protegido['n']} de {esperado}"
out ""
out "== 6. e o que o GIL protege sozinho =="
// 'append' de varias threads entrega tudo: o GIL protege a operacao
// inteira, e avisar sobre ele seria falso alarme em codigo que
// funciona. O que perde e ler-modificar-escrever.
juntos := []
action acrescentar(voltas):
cycle i from 1 to voltas:
juntos.append(i)
parallel:
acrescentar(1000)
acrescentar(1000)
assert len(juntos) is 2000
out " 'append' de duas threads: 2000 de 2000"
out "exercicio 339 ok"parallel espera todas as tarefas e levanta na linha do bloco, com as demais falhas em .outros — então um monitor o pega.
Até a correção, os dois engoliam#
thread e parallel faziam except Exception e imprimiam uma linha: o programa seguia, saía com 0, e nenhum handle via o erro. Um CI passava verde com metade do trabalho perdida.
A linguagem NÃO sincroniza sozinha#
Duas threads escrevendo no mesmo nome perdem atualizações, em silêncio. O check avisa sobre o padrão — mas avisar não é sincronizar.
E o que o GIL protege sozinho#
append de várias threads entrega tudo: o GIL protege a operação inteira, e avisar sobre ele seria falso alarme em código que funciona. O que perde é ler-modificar-escrever.
340 · o que 'monitor' NAO pega#
Enunciado. 'halt', 'skip' e 'yield' atravessam um 'monitor'. Eles
// derivam de um sinal de controle, e nao de erro — capturar um 'yield'
// com 'handle' faria uma acao devolver o tratamento em vez do valor.
out "== 1. 'yield' atravessa =="
action com_monitor():
monitor:
yield "voltei de dentro do monitor"
handle Error:
yield "NAO DEVIA"
assert com_monitor() is "voltei de dentro do monitor"
out ""
out "== 2. e o 'ensure' AINDA roda =="
passos := []
action sai_de_dentro():
monitor:
passos.append("entrou")
yield "saiu"
ensure:
passos.append("limpou")
assert sai_de_dentro() is "saiu"
assert passos is ["entrou", "limpou"]
out ""
out "== 3. 'halt' atravessa =="
encontrados := []
action procurar(xs, alvo):
cycle x in xs:
monitor:
given x is alvo:
halt
encontrados.append(x)
handle Error:
encontrados.append("erro")
yield encontrados
assert procurar([1, 2, 3, 4], 3) is [1, 2]
out ""
out "== 4. 'skip' tambem =="
pares := []
cycle i from 1 to 6:
monitor:
given i % 2 is 1:
skip
pares.append(i)
handle Error:
pares.append(-1)
assert pares is [2, 4, 6]
out ""
out "== 5. e por isso um 'handle' generico nao os engole =="
// Num interpretador que capturasse tudo, o 'halt' viraria uma
// mensagem de erro e o laco continuaria — o defeito mais confuso que
// existe, porque o codigo LE certo.
contou := 0
cycle i from 1 to 10:
monitor:
given i bigger 3:
halt
contou += 1
handle:
contou := -100
assert contou is 3
out ""
out "== 6. o que 'monitor' PEGA =="
// Tudo que deriva de Error. E um 'monitor' sem 'handle' nao engole
// nada: ele so garante o 'ensure'.
escapou := no
monitor:
monitor:
trigger "passa direto"
ensure:
passos.append("mesmo assim")
handle Error:
escapou := yes
assert escapou
assert passos[-1] is "mesmo assim"
out "exercicio 340 ok"halt, skip e yield atravessam um monitor. Eles derivam de um sinal de controle, e não de erro.
Capturar um `yield` faria a ação devolver o tratamento#
Em vez do valor. E o ensure ainda roda — a saída é garantida sem que o sinal seja engolido.
Num interpretador que capturasse tudo#
O halt viraria uma mensagem de erro e o laço continuaria — o defeito mais confuso que existe, porque o código lê certo.
E `monitor` sem `handle` não engole nada#
Ele só garante o ensure.
341 · afirmar que algo FALHA#
Enunciado. um teste que so confere o caminho feliz nao prova nada
// sobre o tratamento. Afirmar a falha e tao importante quanto afirmar
// o sucesso — e afirmar a falha ERRADA e pior que nao afirmar.
adopt Arcane.Crucible as Crucible
steady expect := Crucible.expect
action sacar(saldo, quanto):
given quanto smaller_eq 0.0:
trigger ValorInvalido(quanto)
given quanto bigger saldo:
trigger SaldoInsuficiente(saldo, quanto)
yield saldo - quanto
record ValorInvalido:
valor: Float
record SaldoInsuficiente:
saldo: Float
pedido: Float
out "== 1. afirmar que levanta =="
expect(lambda => sacar(100.0, 250.0)).to_raise()
expect(lambda => sacar(100.0, 30.0)).to_not_raise()
out ""
out "== 2. e afirmar QUAL levanta =="
// Sem isto, um teste passa quando a acao falha pelo motivo errado —
// e o motivo errado costuma ser um erro de digitacao no proprio
// teste.
qual := ""
monitor:
sacar(100.0, -5.0)
handle ValorInvalido:
qual := "valor"
handle SaldoInsuficiente:
qual := "saldo"
assert qual is "valor"
out ""
out "== 3. o teste que passa pelo motivo errado =="
// O nome errado vem de um vault, e nao de uma variavel solta: assim
// ele e o mesmo defeito — um erro de digitacao que so aparece em
// execucao — sem o 'check' recusar o arquivo inteiro, que e o que ele
// faz quando o nome nao existe em lugar nenhum.
steady LIMITES := {"saldo": 100.0}
action com_erro_de_digitacao(saldo, quanto):
given quanto bigger LIMITES["sald"]: // 'sald' nao existe
trigger SaldoInsuficiente(saldo, quanto)
yield saldo - quanto
// 'to_raise' sozinho passaria: a acao levanta mesmo — de NameError.
expect(lambda => com_erro_de_digitacao(100.0, 250.0)).to_raise()
// Conferindo o tipo, o teste reprova como deveria.
tipo := ""
monitor:
com_erro_de_digitacao(100.0, 250.0)
handle Error as e:
tipo := e.type
assert tipo is "KeyError"
assert tipo isnt "TriggerError"
out " 'to_raise' sozinho aprova um teste que deveria falhar"
out ""
out "== 4. conferir a mensagem =="
monitor:
sacar(10.0, 999.0)
handle SaldoInsuficiente as e:
assert e.value.pedido is 999.0
assert e.value.saldo is 10.0
out ""
out "== 5. um caso de borda por vez =="
steady CASOS := [
[100.0, 100.0, "ok"],
[100.0, 100.01, "falta"],
[100.0, 0.0, "invalido"],
[0.0, 1.0, "falta"]
]
action classificar(saldo, quanto):
monitor:
sacar(saldo, quanto)
yield "ok"
handle ValorInvalido:
yield "invalido"
handle SaldoInsuficiente:
yield "falta"
cycle caso in CASOS:
obtido := classificar(caso[0], caso[1])
assert obtido is caso[2], $"{caso[0]}/{caso[1]}: esperava {caso[2]}, veio {obtido}"
out $" {caso[0]} - {caso[1]} -> {obtido}"
out "exercicio 341 ok"Um teste que só confere o caminho feliz não prova nada sobre o tratamento. E afirmar a falha errada é pior que não afirmar.
`to_raise` sozinho aprova um teste que deveria falhar#
Uma ação com erro de digitação levanta mesmo — de NameError. O teste passa, e a regra que ele deveria cobrir nunca foi exercitada.
Conferir o tipo é o que separa os dois#
E o motivo errado costuma ser um erro de digitação no próprio teste.
E um caso de borda por vez#
A tabela de casos diz o esperado ao lado do entrado, e a mensagem do assert diz qual linha falhou.
342 · as dezoito familias, e como escolher#
Enunciado. fechar o modulo com o mapa. O codigo de um erro diz a
// familia, e a familia diz de quem e a culpa — que e a unica pergunta
// que importa quando um log chega as tres da manha.
out "== 1. o codigo carrega a familia =="
action codigo_de(acao):
monitor:
acao()
handle Error as e:
yield e.codigo
yield ""
// DF02xx = execucao, DF06xx = colecoes, DF15xx = validacao.
assert startswith(codigo_de(lambda => 1 / 0), "DF02") // df: permitir division-by-zero
assert startswith(codigo_de(lambda => [1][9]), "DF06") // df: permitir indice-fora-do-alcance
out $" 1/0 -> {codigo_de(lambda => 1 / 0)}" // df: permitir division-by-zero
out ""
out "== 2. as dezoito familias =="
steady FAMILIAS := {
"01": "sintaxe — o texto nao vira programa",
"02": "execucao — o programa roda e falha",
"03": "tipos — o valor nao serve para a operacao",
"04": "nomes — o nome nao existe, ou nao ali",
"05": "modulos — adopt e relay",
"06": "colecoes — indice, chave, fatia",
"07": "do usuario — trigger e assert",
"08": "limites — recursao, memoria, tempo",
"09": "objetos — blueprint, record, trait, enum",
"10": "concorrencia — thread, canal, async",
"11": "sistema — arquivo, processo, ambiente",
"12": "dados — banco, ORM, serializacao",
"13": "rede — HTTP, Kiln, socket",
"14": "testes — Crucible",
"15": "validacao — conversao, formato, contrato",
"16": "dominio — DDD: valor, agregado, evento, regra",
"17": "reativo — sinal, derivado, efeito, observavel",
"18": "memoria estruturada — layout, ponteiro, janela"
}
assert len(keys(FAMILIAS)) is 18
cycle chave in ["02", "16", "17", "18"]:
out $" DF{chave}xx {FAMILIAS[chave]}"
out ""
out "== 3. de quem e a culpa =="
steady CULPA := {
"PreconditionError": "quem chamou",
"PostconditionError": "a acao",
"InvariantError": "o objeto",
"ConversionError": "o dado que entrou",
"FileNotFoundError": "o ambiente",
"TimeoutError": "o outro lado",
"AggregateError": "a regra de negocio"
}
assert CULPA["PreconditionError"] is "quem chamou"
assert CULPA["InvariantError"] is "o objeto"
out " um log que nao responde isso nao serve a ninguem"
out ""
out "== 4. a escada de tratamento =="
// Do mais especifico ao mais geral, e cada degrau com uma acao
// diferente.
action tratar(acao):
monitor:
acao()
yield "ok"
handle ConversionError:
yield "pedir de novo ao usuario"
handle FileNotFoundError:
yield "criar o arquivo e tentar"
handle RuntimeError:
yield "registrar e seguir"
handle Error:
yield "registrar e PARAR"
assert tratar(lambda => int("x")) is "pedir de novo ao usuario"
assert tratar(lambda => 1 / 0) is "registrar e seguir" // df: permitir division-by-zero
assert tratar(lambda => trigger "x") is "registrar e PARAR"
assert tratar(lambda => 1 + 1) is "ok"
out ""
out "== 5. as tres decisoes que este modulo ensinou =="
out " 1. capture pela FAMILIA, e nomeie o especifico quando precisar"
out " 2. falha esperada e VALOR (Resultado); falha excepcional e 'trigger'"
out " 3. o que fecha recurso vai em 'defer' — e ele nao engole nada"
out ""
out "== 6. e a que mais custa caro =="
// 'trigger' levanta TriggerError, e nao RuntimeError. 'handle
// RuntimeError' nao pega um 'trigger' — e esse e o erro que aparece
// como "o tratamento nao funcionou" sem nenhuma pista.
nao_pegou := yes
monitor:
monitor:
trigger "minha falha"
handle RuntimeError:
nao_pegou := no
handle TriggerError:
void
assert nao_pegou
out " 'handle RuntimeError' NAO pega um 'trigger'"
out "exercicio 342 ok"O código de um erro diz a família, e a família diz de quem é a culpa — que é a única pergunta que importa quando um log chega às três da manhã.
Dezoito famílias#
Das três primeiras (sintaxe, execução, tipos) às três últimas (domínio, reativo, memória estruturada).
A escada de tratamento#
Do mais específico ao mais geral, e cada degrau com uma ação diferente: pedir de novo, criar e tentar, registrar e seguir, registrar e parar.
E a que mais custa caro#
handle RuntimeError não pega um trigger.
---
As três decisões que o módulo ensina: capture pela família e nomeie o específico quando precisar; falha esperada é valor, falha excepcional é trigger; e o que fecha recurso vai em defer — que não engole nada.
Rode um isolado com dataforge run exercicios/54-erros/328_familias.df.