@@ -218,3 +218,161 @@ impl Analysis {
218218 self . with_db ( |db| code_action:: code_action ( db, file_id, range, resolve_strategy) )
219219 }
220220}
221+
222+ #[ cfg( test) ]
223+ mod robustness_tests {
224+ use base_db:: { change:: Change , source_root:: SourceRoot } ;
225+ use proptest:: prelude:: * ;
226+ use triomphe:: Arc ;
227+ use utils:: { line_index:: TextSize , lines:: LineEnding } ;
228+ use vfs:: { ChangeKind , ChangedFile , FileId , FileSet , VfsPath } ;
229+
230+ use super :: * ;
231+ use crate :: {
232+ analysis_host:: AnalysisHost ,
233+ completion:: context:: TriggerChar ,
234+ folding_ranges:: FoldingConfig ,
235+ } ;
236+
237+ const FILE_ID : FileId = FileId ( 0 ) ;
238+
239+ fn ascii_source_strategy ( ) -> impl Strategy < Value = String > {
240+ let char_strategy = prop_oneof ! [
241+ Just ( ' ' ) ,
242+ Just ( '\n' ) ,
243+ Just ( '\t' ) ,
244+ Just ( '(' ) ,
245+ Just ( ')' ) ,
246+ Just ( '{' ) ,
247+ Just ( '}' ) ,
248+ Just ( ';' ) ,
249+ Just ( ':' ) ,
250+ Just ( ',' ) ,
251+ Just ( '.' ) ,
252+ Just ( '#' ) ,
253+ Just ( '`' ) ,
254+ Just ( '_' ) ,
255+ ( b'a' ..=b'z' ) . prop_map( |byte| byte as char ) ,
256+ ( b'A' ..=b'Z' ) . prop_map( |byte| byte as char ) ,
257+ ( b'0' ..=b'9' ) . prop_map( |byte| byte as char ) ,
258+ ] ;
259+
260+ prop:: collection:: vec ( char_strategy, 0 ..256 ) . prop_map ( |chars| chars. into_iter ( ) . collect ( ) )
261+ }
262+
263+ fn edit_sequence_strategy ( ) -> impl Strategy < Value = Vec < ( u16 , u8 , char ) > > {
264+ let char_strategy = prop_oneof ! [
265+ Just ( ' ' ) ,
266+ Just ( '\n' ) ,
267+ Just ( '\t' ) ,
268+ Just ( '(' ) ,
269+ Just ( ')' ) ,
270+ Just ( '{' ) ,
271+ Just ( '}' ) ,
272+ Just ( ';' ) ,
273+ Just ( ':' ) ,
274+ Just ( ',' ) ,
275+ Just ( '.' ) ,
276+ Just ( '#' ) ,
277+ Just ( '`' ) ,
278+ Just ( '_' ) ,
279+ ( b'a' ..=b'z' ) . prop_map( |byte| byte as char ) ,
280+ ( b'A' ..=b'Z' ) . prop_map( |byte| byte as char ) ,
281+ ( b'0' ..=b'9' ) . prop_map( |byte| byte as char ) ,
282+ ] ;
283+
284+ prop:: collection:: vec ( ( any :: < u16 > ( ) , 0u8 ..3 , char_strategy) , 0 ..48 )
285+ }
286+
287+ fn trigger_strategy ( ) -> impl Strategy < Value = Option < TriggerChar > > {
288+ prop_oneof ! [
289+ Just ( None ) ,
290+ Just ( Some ( TriggerChar :: Dot ) ) ,
291+ Just ( Some ( TriggerChar :: OpenParen ) ) ,
292+ Just ( Some ( TriggerChar :: Comma ) ) ,
293+ Just ( Some ( TriggerChar :: At ) ) ,
294+ Just ( Some ( TriggerChar :: Hash ) ) ,
295+ Just ( Some ( TriggerChar :: Backtick ) ) ,
296+ ]
297+ }
298+
299+ fn apply_edit ( text : & mut String , position_seed : u16 , kind : u8 , ch : char ) {
300+ match kind {
301+ 0 => {
302+ let idx = usize:: from ( position_seed) % ( text. len ( ) + 1 ) ;
303+ text. insert ( idx, ch) ;
304+ }
305+ 1 => {
306+ if !text. is_empty ( ) {
307+ let idx = usize:: from ( position_seed) % text. len ( ) ;
308+ text. remove ( idx) ;
309+ }
310+ }
311+ 2 => {
312+ if text. is_empty ( ) {
313+ text. push ( ch) ;
314+ } else {
315+ let idx = usize:: from ( position_seed) % text. len ( ) ;
316+ text. remove ( idx) ;
317+ text. insert ( idx, ch) ;
318+ }
319+ }
320+ _ => unreachable ! ( ) ,
321+ }
322+ }
323+
324+ fn setup_host ( text : & str ) -> AnalysisHost {
325+ let path = VfsPath :: new_virtual_path ( "/robustness.sv" . to_string ( ) ) ;
326+ let mut file_set = FileSet :: default ( ) ;
327+ file_set. insert ( FILE_ID , path) ;
328+
329+ let root = SourceRoot :: new_local ( file_set) ;
330+ let mut change = Change :: new ( ) ;
331+ change. set_roots ( vec ! [ root] ) ;
332+ change. add_changed_file ( ChangedFile {
333+ file_id : FILE_ID ,
334+ change_kind : ChangeKind :: Create ( Arc :: from ( text) , LineEnding :: Unix ) ,
335+ } ) ;
336+
337+ let mut host = AnalysisHost :: default ( ) ;
338+ host. apply_change ( change) ;
339+ host
340+ }
341+
342+ fn update_host ( host : & mut AnalysisHost , text : & str ) {
343+ let mut change = Change :: new ( ) ;
344+ change. add_changed_file ( ChangedFile {
345+ file_id : FILE_ID ,
346+ change_kind : ChangeKind :: Modify ( Arc :: from ( text) , LineEnding :: Unix ) ,
347+ } ) ;
348+ host. apply_change ( change) ;
349+ }
350+
351+ proptest ! {
352+ #[ test]
353+ fn analysis_requests_survive_random_ascii_edits(
354+ initial_text in ascii_source_strategy( ) ,
355+ edits in edit_sequence_strategy( ) ,
356+ trigger in trigger_strategy( ) ,
357+ ) {
358+ let mut text = initial_text;
359+ let mut host = setup_host( & text) ;
360+
361+ for ( position_seed, kind, ch) in edits {
362+ apply_edit( & mut text, position_seed, kind, ch) ;
363+ update_host( & mut host, & text) ;
364+
365+ let analysis = host. make_analysis( ) ;
366+ let _ = analysis. parse_diagnostics( FILE_ID ) ?;
367+ let _ = analysis. document_symbol( FILE_ID ) ?;
368+ let _ = analysis. folding_ranges( FILE_ID , & FoldingConfig { line_fold_only: false } ) ?;
369+
370+ for offset in [ 0 , text. len( ) / 2 , text. len( ) ] {
371+ let position = FilePosition { file_id: FILE_ID , offset: TextSize :: from( offset as u32 ) } ;
372+ let _ = analysis. completion_context_with_trigger( position, trigger) ?;
373+ let _ = analysis. completions_with_trigger( position, trigger) ?;
374+ }
375+ }
376+ }
377+ }
378+ }
0 commit comments