light_program_test/logging/
mod.rs1use std::{
12 collections::HashMap,
13 fs::OpenOptions,
14 io::Write,
15 path::PathBuf,
16 time::{SystemTime, UNIX_EPOCH},
17};
18
19use chrono;
20pub use light_instruction_decoder::{
22 AccountAccess, AccountChange, AccountCompressionInstructionDecoder, AccountStateSnapshot,
23 CTokenInstructionDecoder, Colors, CompressedAccountInfo, ComputeBudgetInstructionDecoder,
24 DecodedField, DecodedInstruction, DecoderRegistry, EnhancedInstructionLog,
25 EnhancedLoggingConfig, EnhancedTransactionLog, InstructionDecoder, LightProtocolEvent,
26 LightSystemInstructionDecoder, LogVerbosity, MerkleTreeChange, RegistryInstructionDecoder,
27 SplTokenInstructionDecoder, SystemInstructionDecoder, Token2022InstructionDecoder,
28 TransactionFormatter, TransactionStatus,
29};
30use litesvm::{types::TransactionResult, LiteSVM};
31use solana_sdk::{
32 inner_instruction::InnerInstruction, pubkey::Pubkey, signature::Signature,
33 transaction::Transaction,
34};
35
36pub type AccountStates = HashMap<Pubkey, (u64, usize, Pubkey)>;
39
40pub fn capture_account_states(context: &LiteSVM, transaction: &Transaction) -> AccountStates {
43 let mut states = HashMap::new();
44 for pubkey in &transaction.message.account_keys {
45 if let Some(account) = context.get_account(pubkey) {
46 states.insert(
47 *pubkey,
48 (account.lamports, account.data.len(), account.owner),
49 );
50 } else {
51 states.insert(*pubkey, (0, 0, Pubkey::default()));
52 }
53 }
54 states
55}
56
57use crate::program_test::config::ProgramTestConfig;
58
59static SESSION_STARTED: std::sync::Once = std::sync::Once::new();
60
61fn get_log_file_path() -> PathBuf {
63 use std::process::Command;
64 if let Ok(output) = Command::new("cargo")
65 .arg("metadata")
66 .arg("--format-version=1")
67 .arg("--no-deps")
68 .output()
69 {
70 if output.status.success() {
71 if let Ok(metadata) = String::from_utf8(output.stdout) {
72 if let Ok(json) = serde_json::from_str::<serde_json::Value>(&metadata) {
73 if let Some(target_directory) = json["target_directory"].as_str() {
74 let mut path = PathBuf::from(target_directory);
75 path.push("light_program_test.log");
76 return path;
77 }
78 }
79 }
80 }
81 }
82
83 let mut path = PathBuf::from("target");
84 path.push("light_program_test.log");
85 path
86}
87
88fn initialize_log_file() {
90 SESSION_STARTED.call_once(|| {
91 let log_path = get_log_file_path();
92 let timestamp = SystemTime::now()
93 .duration_since(UNIX_EPOCH)
94 .unwrap_or_default()
95 .as_secs();
96
97 if let Ok(mut file) = OpenOptions::new()
98 .create(true)
99 .write(true)
100 .truncate(true)
101 .open(&log_path)
102 {
103 let datetime =
104 chrono::DateTime::from_timestamp(timestamp as i64, 0).unwrap_or(chrono::Utc::now());
105 let formatted_date = datetime.format("%Y-%m-%d %H:%M:%S UTC");
106
107 let _ = writeln!(
108 file,
109 "=== Light Program Test Session Started at {} ===\n",
110 formatted_date
111 );
112 }
113 });
114}
115
116fn strip_ansi_codes(text: &str) -> String {
118 let mut result = String::with_capacity(text.len());
119 let mut chars = text.chars();
120
121 while let Some(ch) = chars.next() {
122 if ch == '\x1b' {
123 for next_ch in chars.by_ref() {
124 if next_ch == 'm' {
125 break;
126 }
127 }
128 } else {
129 result.push(ch);
130 }
131 }
132
133 result
134}
135
136fn write_to_log_file(content: &str) {
138 initialize_log_file();
139
140 let log_path = get_log_file_path();
141
142 if let Some(parent) = log_path.parent() {
143 let _ = std::fs::create_dir_all(parent);
144 }
145
146 let clean_content = strip_ansi_codes(content);
147
148 if let Ok(mut file) = OpenOptions::new().create(true).append(true).open(&log_path) {
149 let _ = writeln!(file, "{}", clean_content);
150 }
151}
152
153#[allow(clippy::too_many_arguments)]
155pub fn log_transaction_enhanced(
156 config: &ProgramTestConfig,
157 transaction: &Transaction,
158 result: &TransactionResult,
159 signature: &Signature,
160 slot: u64,
161 transaction_counter: usize,
162 pre_states: Option<&AccountStates>,
163 post_states: Option<&AccountStates>,
164) {
165 log_transaction_enhanced_with_console(
166 config,
167 transaction,
168 result,
169 signature,
170 slot,
171 transaction_counter,
172 false,
173 pre_states,
174 post_states,
175 )
176}
177
178#[allow(clippy::too_many_arguments)]
180pub fn log_transaction_enhanced_with_console(
181 config: &ProgramTestConfig,
182 transaction: &Transaction,
183 result: &TransactionResult,
184 signature: &Signature,
185 slot: u64,
186 transaction_counter: usize,
187 print_to_console: bool,
188 pre_states: Option<&AccountStates>,
189 post_states: Option<&AccountStates>,
190) {
191 if !config.enhanced_logging.enabled {
192 return;
193 }
194
195 let enhanced_log = from_transaction_result(
196 transaction,
197 result,
198 signature,
199 slot,
200 &config.enhanced_logging,
201 pre_states,
202 post_states,
203 );
204
205 let formatter = TransactionFormatter::new(&config.enhanced_logging);
206 let formatted_log = formatter.format(&enhanced_log, transaction_counter);
207
208 write_to_log_file(&formatted_log);
209
210 if print_to_console {
211 println!("{}", formatted_log);
212 }
213}
214
215pub fn should_use_enhanced_logging(config: &ProgramTestConfig) -> bool {
217 config.enhanced_logging.enabled && !config.no_logs
218}
219
220fn get_program_name(program_id: &Pubkey) -> String {
226 light_instruction_decoder::types::get_program_name(
227 &solana_pubkey::Pubkey::new_from_array(program_id.to_bytes()),
228 None,
229 )
230}
231
232fn get_pretty_logs_string(result: &TransactionResult) -> String {
234 match result {
235 Ok(meta) => meta.pretty_logs(),
236 Err(failed) => failed.meta.pretty_logs(),
237 }
238}
239
240pub fn from_transaction_result(
247 transaction: &Transaction,
248 result: &TransactionResult,
249 signature: &Signature,
250 slot: u64,
251 config: &EnhancedLoggingConfig,
252 pre_states: Option<&AccountStates>,
253 post_states: Option<&AccountStates>,
254) -> EnhancedTransactionLog {
255 let (status, compute_consumed) = match result {
256 Ok(meta) => (TransactionStatus::Success, meta.compute_units_consumed),
257 Err(failed) => (
258 TransactionStatus::Failed(format!("{:?}", failed.err)),
259 failed.meta.compute_units_consumed,
260 ),
261 };
262
263 let estimated_fee = (transaction.signatures.len() as u64) * 5000;
264
265 let account_states = if let (Some(pre), Some(post)) = (pre_states, post_states) {
267 let mut states = HashMap::new();
268 for pubkey in &transaction.message.account_keys {
269 let (lamports_before, data_len_before, _) =
270 pre.get(pubkey)
271 .copied()
272 .unwrap_or((0, 0, Pubkey::default()));
273 let (lamports_after, data_len_after, owner) =
274 post.get(pubkey)
275 .copied()
276 .unwrap_or((0, 0, Pubkey::default()));
277
278 states.insert(
279 solana_pubkey::Pubkey::new_from_array(pubkey.to_bytes()),
280 AccountStateSnapshot {
281 lamports_before,
282 lamports_after,
283 data_len_before,
284 data_len_after,
285 owner: solana_pubkey::Pubkey::new_from_array(owner.to_bytes()),
286 },
287 );
288 }
289 Some(states)
290 } else {
291 None
292 };
293
294 let mut instructions: Vec<EnhancedInstructionLog> = transaction
296 .message
297 .instructions
298 .iter()
299 .enumerate()
300 .map(|(index, ix)| {
301 let program_id = transaction.message.account_keys[ix.program_id_index as usize];
302 let mut log = EnhancedInstructionLog::new(
303 index,
304 solana_pubkey::Pubkey::new_from_array(program_id.to_bytes()),
305 get_program_name(&program_id),
306 );
307 log.accounts = ix
308 .accounts
309 .iter()
310 .map(|&idx| {
311 let pubkey = transaction.message.account_keys[idx as usize];
312 solana_instruction::AccountMeta {
313 pubkey: solana_pubkey::Pubkey::new_from_array(pubkey.to_bytes()),
314 is_signer: transaction.message.is_signer(idx as usize),
315 is_writable: transaction.message.is_maybe_writable(idx as usize, None),
316 }
317 })
318 .collect();
319 log.data = ix.data.clone();
320 log
321 })
322 .collect();
323
324 let inner_instructions_list = match result {
326 Ok(meta) => &meta.inner_instructions,
327 Err(failed) => &failed.meta.inner_instructions,
328 };
329
330 if config.decode_light_instructions {
332 for instruction in instructions.iter_mut() {
333 instruction.decode(config);
334 }
335
336 for (instruction_index, inner_list) in inner_instructions_list.iter().enumerate() {
338 if let Some(instruction) = instructions.get_mut(instruction_index) {
339 instruction.inner_instructions = parse_inner_instructions(
340 inner_list,
341 &transaction.message.account_keys,
342 &transaction.message,
343 1,
344 config,
345 );
346 }
347 }
348 }
349
350 let pretty_logs_string = get_pretty_logs_string(result);
351
352 let sig_bytes: [u8; 64] = signature.as_ref().try_into().unwrap_or([0u8; 64]);
353 let mut log = EnhancedTransactionLog::new(
354 light_instruction_decoder::solana_signature::Signature::from(sig_bytes),
355 slot,
356 );
357 log.status = status;
358 log.fee = estimated_fee;
359 log.compute_used = compute_consumed;
360 log.instructions = instructions;
361 log.program_logs_pretty = pretty_logs_string;
362 log.account_states = account_states;
363 log
364}
365
366fn parse_inner_instructions(
368 inner_instructions: &[InnerInstruction],
369 account_keys: &[Pubkey],
370 message: &solana_sdk::message::Message,
371 base_depth: usize,
372 config: &EnhancedLoggingConfig,
373) -> Vec<EnhancedInstructionLog> {
374 let mut result = Vec::new();
375
376 for (index, inner_ix) in inner_instructions.iter().enumerate() {
377 let program_id = account_keys[inner_ix.instruction.program_id_index as usize];
378 let program_name = get_program_name(&program_id);
379
380 let accounts: Vec<solana_instruction::AccountMeta> = inner_ix
381 .instruction
382 .accounts
383 .iter()
384 .map(|&idx| {
385 let account_index = idx as usize;
386 let pubkey = account_keys[account_index];
387
388 let is_signer = message.is_signer(account_index);
389 let is_writable = message.is_maybe_writable(account_index, None);
390
391 solana_instruction::AccountMeta {
392 pubkey: solana_pubkey::Pubkey::new_from_array(pubkey.to_bytes()),
393 is_signer,
394 is_writable,
395 }
396 })
397 .collect();
398
399 let instruction_depth = base_depth + (inner_ix.stack_height as usize).saturating_sub(1);
400
401 let mut instruction_log = EnhancedInstructionLog::new(
402 index,
403 solana_pubkey::Pubkey::new_from_array(program_id.to_bytes()),
404 program_name,
405 );
406 instruction_log.accounts = accounts;
407 instruction_log.data = inner_ix.instruction.data.clone();
408 instruction_log.depth = instruction_depth;
409
410 if config.decode_light_instructions {
412 instruction_log.decode(config);
413 }
414
415 if inner_ix.stack_height <= 2 {
417 result.push(instruction_log);
418 } else {
419 let target_parent_depth = instruction_depth - 1;
420 if let Some(parent) = EnhancedInstructionLog::find_parent_for_instruction(
421 &mut result,
422 target_parent_depth,
423 ) {
424 parent.inner_instructions.push(instruction_log);
425 } else {
426 result.push(instruction_log);
427 }
428 }
429 }
430
431 result
432}