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